/*
 * Copyright 2005-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
 *
 *      https://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.ldap.core;

import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.LdapDataEntry;
import org.springframework.dao.EmptyResultDataAccessException;
import org.springframework.dao.IncorrectResultSizeDataAccessException;
import org.springframework.ldap.LimitExceededException;
import org.springframework.ldap.NameNotFoundException;
import org.springframework.ldap.PartialResultException;
import org.springframework.ldap.UncategorizedLdapException;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.odm.core.ObjectDirectoryMapper;
import org.springframework.ldap.query.LdapQuery;
import org.springframework.ldap.query.LdapQueryBuilder;
import org.springframework.ldap.support.LdapUtils;

import javax.naming.Binding;
import javax.naming.CompositeName;
import javax.naming.Name;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.BasicAttributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.LdapName;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
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 static org.springframework.ldap.query.LdapQueryBuilder.query;

/**
 * Unit tests for the LdapTemplate class.
 * 
 * @author Mattias Hellborg Arthursson
 * @author Ulrik Sandberg
 */
public class LdapTemplateTest {

	private static final String DEFAULT_BASE_STRING = "o=example.com";

	private ContextSource contextSourceMock;

	private DirContext dirContextMock;

	private AttributesMapper attributesMapperMock;

	private NamingEnumeration namingEnumerationMock;

	private Name nameMock;

	private NameClassPairCallbackHandler handlerMock;

	private ContextMapper contextMapperMock;

	private ContextExecutor contextExecutorMock;

	private SearchExecutor searchExecutorMock;

	private LdapTemplate tested;

	private DirContextProcessor dirContextProcessorMock;

	private DirContextOperations dirContextOperationsMock;

	private DirContext authenticatedContextMock;

	private AuthenticatedLdapEntryContextCallback entryContextCallbackMock;
    private ObjectDirectoryMapper odmMock;

	private LdapQuery query;
	private AuthenticatedLdapEntryContextMapper authContextMapperMock;

	@Before
	public void setUp() throws Exception {

		// Setup ContextSource mock
		contextSourceMock = mock(ContextSource.class);
		// Setup LdapContext mock
		dirContextMock = mock(LdapContext.class);
		// Setup NamingEnumeration mock
		namingEnumerationMock = mock(NamingEnumeration.class);
		// Setup Name mock
		nameMock = LdapUtils.emptyLdapName();
		// Setup Handler mock
		handlerMock = mock(NameClassPairCallbackHandler.class);
		contextMapperMock = mock(ContextMapper.class);
		attributesMapperMock = mock(AttributesMapper.class);
		contextExecutorMock = mock(ContextExecutor.class);
		searchExecutorMock = mock(SearchExecutor.class);
		dirContextProcessorMock = mock(DirContextProcessor.class);
		dirContextOperationsMock = mock(DirContextOperations.class);
		authenticatedContextMock = mock(DirContext.class);
		entryContextCallbackMock = mock(AuthenticatedLdapEntryContextCallback.class);
        odmMock = mock(ObjectDirectoryMapper.class);
		query = LdapQueryBuilder.query().base("ou=spring").filter("ou=user");
		authContextMapperMock = mock(AuthenticatedLdapEntryContextMapper.class);

        tested = new LdapTemplate(contextSourceMock);
        tested.setObjectDirectoryMapper(odmMock);
	}

	private void expectGetReadWriteContext() {
		when(contextSourceMock.getReadWriteContext()).thenReturn(dirContextMock);
	}

	private void expectGetReadOnlyContext() {
		when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock);
	}

    @Test
	public void testSearch_CallbackHandler() throws Exception {
		expectGetReadOnlyContext();

		SearchResult searchResult = new SearchResult("", new Object(), new BasicAttributes());

		singleSearchResult(searchControlsOneLevel(), searchResult);

		tested.search(nameMock, "(ou=somevalue)", 1, true, handlerMock);

        verify(handlerMock).handleNameClassPair(searchResult);
        verify(dirContextMock).close();
	}

    @Test
	public void testSearch_StringBase_CallbackHandler() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsOneLevel();

		SearchResult searchResult = new SearchResult("", new Object(), new BasicAttributes());

		singleSearchResultWithStringBase(controls, searchResult);

		tested.search(DEFAULT_BASE_STRING, "(ou=somevalue)", 1, true, handlerMock);

        verify(handlerMock).handleNameClassPair(searchResult);
        verify(dirContextMock).close();
	}

    @Test
	public void testSearch_CallbackHandler_Defaults() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsRecursive();
		controls.setReturningObjFlag(false);

		SearchResult searchResult = new SearchResult("", new Object(), new BasicAttributes());

		singleSearchResult(controls, searchResult);

		tested.search(nameMock, "(ou=somevalue)", handlerMock);

        verify(handlerMock).handleNameClassPair(searchResult);
        verify(dirContextMock).close();
	}

    @Test
	public void testSearch_String_CallbackHandler_Defaults() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsRecursive();
		controls.setReturningObjFlag(false);

		SearchResult searchResult = new SearchResult("", new Object(), new BasicAttributes());

		singleSearchResultWithStringBase(controls, searchResult);

		tested.search(DEFAULT_BASE_STRING, "(ou=somevalue)", handlerMock);

        verify(handlerMock).handleNameClassPair(searchResult);
        verify(dirContextMock).close();
	}

    @Test
	public void testSearch_NameNotFoundException() throws Exception {
		expectGetReadOnlyContext();

		final SearchControls controls = searchControlsRecursive();
		controls.setReturningObjFlag(false);

		javax.naming.NameNotFoundException ne = new javax.naming.NameNotFoundException("some text");
		when(dirContextMock.search(
                eq(nameMock),
                eq("(ou=somevalue)"),
                argThat(new SearchControlsMatcher(controls)))).thenThrow(ne);

		try {
			tested.search(nameMock, "(ou=somevalue)", handlerMock);
			fail("NameNotFoundException expected");
		}
		catch (NameNotFoundException expected) {
			assertThat(true).isTrue();
		}
		verify(dirContextMock).close();
	}

    @Test
	public void testSearch_NamingException() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsRecursive();
		controls.setReturningObjFlag(false);

		javax.naming.LimitExceededException ne = new javax.naming.LimitExceededException();
        when(dirContextMock.search(
                eq(nameMock),
                eq("(ou=somevalue)"),
                argThat(new SearchControlsMatcher(controls)))).thenThrow(ne);

		try {
			tested.search(nameMock, "(ou=somevalue)", handlerMock);
			fail("LimitExceededException expected");
		}
		catch (LimitExceededException expected) {
			// expected
		}

        verify(dirContextMock).close();
	}

    @Test
	public void testSearch_CallbackHandler_DirContextProcessor() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsRecursive();
		controls.setReturningObjFlag(false);

		SearchResult searchResult = new SearchResult("", new Object(), new BasicAttributes());

		singleSearchResult(controls, searchResult);

		tested.search(nameMock, "(ou=somevalue)", controls, handlerMock, dirContextProcessorMock);

        verify(dirContextProcessorMock).preProcess(dirContextMock);
        verify(dirContextProcessorMock).postProcess(dirContextMock);
        verify(namingEnumerationMock).close();
        verify(handlerMock).handleNameClassPair(searchResult);
        verify(dirContextMock).close();
	}

    @Test
	public void testSearch_String_CallbackHandler_DirContextProcessor() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsRecursive();
		controls.setReturningObjFlag(false);

		SearchResult searchResult = new SearchResult("", new Object(), new BasicAttributes());

		singleSearchResultWithStringBase(controls, searchResult);

		tested.search(DEFAULT_BASE_STRING, "(ou=somevalue)", controls, handlerMock, dirContextProcessorMock);

        verify(dirContextProcessorMock).preProcess(dirContextMock);
        verify(dirContextProcessorMock).postProcess(dirContextMock);
        verify(namingEnumerationMock).close();
        verify(handlerMock).handleNameClassPair(searchResult);
        verify(dirContextMock).close();
	}

    @Test
	public void testSearch_String_AttributesMapper_DirContextProcessor() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsOneLevel();
		controls.setReturningObjFlag(false);

		BasicAttributes expectedAttributes = new BasicAttributes();
		SearchResult searchResult = new SearchResult("", null, expectedAttributes);

		singleSearchResultWithStringBase(controls, searchResult);

		Object expectedResult = new Object();
		when(attributesMapperMock.mapFromAttributes(expectedAttributes)).thenReturn(expectedResult);

		List list = tested.search(DEFAULT_BASE_STRING, "(ou=somevalue)", controls, attributesMapperMock,
				dirContextProcessorMock);

        verify(dirContextProcessorMock).preProcess(dirContextMock);
        verify(dirContextProcessorMock).postProcess(dirContextMock);
        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_Name_AttributesMapper_DirContextProcessor() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsOneLevel();
		controls.setReturningObjFlag(false);

		BasicAttributes expectedAttributes = new BasicAttributes();
		SearchResult searchResult = new SearchResult("", null, expectedAttributes);

		singleSearchResult(controls, searchResult);

		Object expectedResult = new Object();
		when(attributesMapperMock.mapFromAttributes(expectedAttributes)).thenReturn(expectedResult);

		List list = tested.search(nameMock, "(ou=somevalue)", controls, attributesMapperMock, dirContextProcessorMock);

        verify(dirContextProcessorMock).preProcess(dirContextMock);
        verify(dirContextProcessorMock).postProcess(dirContextMock);
        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_String_SearchControls_ContextMapper_DirContextProcessor() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsRecursive();

		Object expectedObject = new Object();
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		singleSearchResultWithStringBase(controls, searchResult);

		Object expectedResult = expectedObject;
		when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult);

		List list = tested.search(DEFAULT_BASE_STRING, "(ou=somevalue)", controls, contextMapperMock,
				dirContextProcessorMock);

        verify(dirContextProcessorMock).preProcess(dirContextMock);
        verify(dirContextProcessorMock).postProcess(dirContextMock);
        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

		assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_Name_SearchControls_ContextMapper_DirContextProcessor() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsRecursive();

		Object expectedObject = new Object();
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		singleSearchResult(controls, searchResult);

		Object expectedResult = expectedObject;
		when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult);

		List list = tested.search(nameMock, "(ou=somevalue)", controls, contextMapperMock, dirContextProcessorMock);

        verify(dirContextProcessorMock).preProcess(dirContextMock);
        verify(dirContextProcessorMock).postProcess(dirContextMock);
        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_AttributesMapper_ReturningAttrs() throws Exception {
		expectGetReadOnlyContext();

		String[] attrs = new String[0];
		SearchControls controls = new SearchControls();
		controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
		controls.setReturningObjFlag(false);
		controls.setReturningAttributes(attrs);

		BasicAttributes expectedAttributes = new BasicAttributes();
		SearchResult searchResult = new SearchResult("", null, expectedAttributes);

		singleSearchResult(controls, searchResult);

		Object expectedResult = new Object();
		when(attributesMapperMock.mapFromAttributes(expectedAttributes)).thenReturn(expectedResult);

		List list = tested.search(nameMock, "(ou=somevalue)", 1, attrs, attributesMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_String_AttributesMapper_ReturningAttrs() throws Exception {
		expectGetReadOnlyContext();

		String[] attrs = new String[0];
		SearchControls controls = new SearchControls();
		controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
		controls.setReturningObjFlag(false);
		controls.setReturningAttributes(attrs);

		BasicAttributes expectedAttributes = new BasicAttributes();
		SearchResult searchResult = new SearchResult("", null, expectedAttributes);

		singleSearchResultWithStringBase(controls, searchResult);

		Object expectedResult = new Object();
		when(attributesMapperMock.mapFromAttributes(expectedAttributes)).thenReturn(expectedResult);

		List list = tested.search(DEFAULT_BASE_STRING, "(ou=somevalue)", 1, attrs, attributesMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
    public void verifyThatDefaultSearchControlParametersAreAutomaticallyAppliedInSearch() throws Exception {
        tested.setDefaultSearchScope(SearchControls.ONELEVEL_SCOPE);
        tested.setDefaultCountLimit(5000);
        tested.setDefaultTimeLimit(500);

        expectGetReadOnlyContext();

        SearchControls controls = new SearchControls();
        controls.setReturningObjFlag(false);
        controls.setCountLimit(5000);
        controls.setTimeLimit(500);
        controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);

        BasicAttributes expectedAttributes = new BasicAttributes();
        SearchResult searchResult = new SearchResult("", null, expectedAttributes);

        singleSearchResult(controls, searchResult);

        Object expectedResult = new Object();
        when(attributesMapperMock.mapFromAttributes(expectedAttributes)).thenReturn(expectedResult);

        List list = tested.search(nameMock, "(ou=somevalue)", attributesMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
        assertThat(list).hasSize(1);
        assertThat(list.get(0)).isSameAs(expectedResult);
    }

    @Test
	public void testSearch_AttributesMapper() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsOneLevel();
		controls.setReturningObjFlag(false);

		BasicAttributes expectedAttributes = new BasicAttributes();
		SearchResult searchResult = new SearchResult("", null, expectedAttributes);

		singleSearchResult(controls, searchResult);

		Object expectedResult = new Object();
		when(attributesMapperMock.mapFromAttributes(expectedAttributes)).thenReturn(expectedResult);

		List list = tested.search(nameMock, "(ou=somevalue)", 1, attributesMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

		assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_String_AttributesMapper() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsOneLevel();
		controls.setReturningObjFlag(false);

		BasicAttributes expectedAttributes = new BasicAttributes();
		SearchResult searchResult = new SearchResult("", null, expectedAttributes);

		singleSearchResultWithStringBase(controls, searchResult);

		Object expectedResult = new Object();
		when(attributesMapperMock.mapFromAttributes(expectedAttributes)).thenReturn(expectedResult);

		List list = tested.search(DEFAULT_BASE_STRING, "(ou=somevalue)", 1, attributesMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_AttributesMapper_Default() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsRecursive();
		controls.setReturningObjFlag(false);

		BasicAttributes expectedAttributes = new BasicAttributes();
		SearchResult searchResult = new SearchResult("", null, expectedAttributes);

		singleSearchResult(controls, searchResult);

		Object expectedResult = new Object();
		when(attributesMapperMock.mapFromAttributes(expectedAttributes)).thenReturn(expectedResult);

		List list = tested.search(nameMock, "(ou=somevalue)", attributesMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_String_AttributesMapper_Default() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsRecursive();
		controls.setReturningObjFlag(false);

		BasicAttributes expectedAttributes = new BasicAttributes();
		SearchResult searchResult = new SearchResult("", null, expectedAttributes);

		singleSearchResultWithStringBase(controls, searchResult);

		Object expectedResult = new Object();
		when(attributesMapperMock.mapFromAttributes(expectedAttributes)).thenReturn(expectedResult);

		List list = tested.search(DEFAULT_BASE_STRING, "(ou=somevalue)", attributesMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_ContextMapper() throws Exception {
		expectGetReadOnlyContext();

		Object expectedObject = new Object();
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());
		singleSearchResult(searchControlsOneLevel(), searchResult);

		Object expectedResult = expectedObject;
		when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult);

		List list = tested.search(nameMock, "(ou=somevalue)", 1, contextMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
    public void testFindOne() throws Exception {
        Class<Object> expectedClass = Object.class;

        when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock);
        when(odmMock.filterFor(expectedClass,
                new EqualsFilter("ou", "somevalue"))).thenReturn(new EqualsFilter("ou", "somevalue"));

        DirContextAdapter expectedObject = new DirContextAdapter();
        SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());
        singleSearchResult(searchControlsRecursive(), searchResult);

        Object expectedResult = expectedObject;
        when(odmMock.mapFromLdapDataEntry(expectedObject, expectedClass)).thenReturn(expectedResult);

        Object result = tested.findOne(query()
                .where("ou").is("somevalue"), expectedClass);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(result).isSameAs(expectedResult);
    }

    @Test
    public void verifyThatFindOneThrowsEmptyResultIfNoResult() throws Exception {
        Class<Object> expectedClass = Object.class;

        when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock);
        when(odmMock.filterFor(expectedClass,
                new EqualsFilter("ou", "somevalue"))).thenReturn(new EqualsFilter("ou", "somevalue"));

        noSearchResults(searchControlsRecursive());

        try {
            tested.findOne(query().where("ou").is("somevalue"), expectedClass);
            fail("EmptyResultDataAccessException expected");
        } catch (EmptyResultDataAccessException expected) {
            assertThat(true).isTrue();
        }

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();
        verify(odmMock, never()).mapFromLdapDataEntry(any(LdapDataEntry.class), any(Class.class));
    }

    @Test
    public void verifyThatFindOneThrowsIncorrectResultSizeDataAccessExceptionWhenMoreResults() throws Exception {
        Class<Object> expectedClass = Object.class;

        when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock);
        when(odmMock.filterFor(expectedClass,
                new EqualsFilter("ou", "somevalue"))).thenReturn(new EqualsFilter("ou", "somevalue"));

        DirContextAdapter expectedObject = new DirContextAdapter();
        SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

        setupSearchResults(searchControlsRecursive(), new SearchResult[]{searchResult, searchResult});

        Object expectedResult = expectedObject;
        when(odmMock.mapFromLdapDataEntry(expectedObject, expectedClass)).thenReturn(expectedResult, expectedResult);

        try {
            tested.findOne(query().where("ou").is("somevalue"), expectedClass);
            fail("EmptyResultDataAccessException expected");
        } catch (IncorrectResultSizeDataAccessException expected) {
            assertThat(true).isTrue();
        }

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();
    }

    @Test
	public void testSearch_ContextMapper_ReturningAttrs() throws Exception {
		expectGetReadOnlyContext();

		String[] attrs = new String[0];

		SearchControls controls = searchControlsOneLevel();
		controls.setReturningAttributes(attrs);

		Object expectedObject = new Object();
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		singleSearchResult(controls, searchResult);

		Object expectedResult = expectedObject;
		when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult);

		List list = tested.search(nameMock, "(ou=somevalue)", 1, attrs, contextMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_String_ContextMapper_ReturningAttrs() throws Exception {
		expectGetReadOnlyContext();

		String[] attrs = new String[0];

		SearchControls controls = searchControlsOneLevel();
		controls.setReturningAttributes(attrs);

		Object expectedObject = new Object();
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		singleSearchResultWithStringBase(controls, searchResult);

		Object expectedResult = expectedObject;
		when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult);

		List list = tested.search(DEFAULT_BASE_STRING, "(ou=somevalue)", 1, attrs, contextMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_String_ContextMapper() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsOneLevel();

		Object expectedObject = new Object();
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		singleSearchResultWithStringBase(controls, searchResult);

		Object expectedResult = expectedObject;
		when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult);


		List list = tested.search(DEFAULT_BASE_STRING, "(ou=somevalue)", 1, contextMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_ContextMapper_Default() throws Exception {
		expectGetReadOnlyContext();

		Object expectedObject = new Object();
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		singleSearchResult(searchControlsRecursive(), searchResult);

		Object expectedResult = expectedObject;
		when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult);

		List list = tested.search(nameMock, "(ou=somevalue)", contextMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_String_ContextMapper_Default() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsRecursive();

		Object expectedObject = new Object();
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		singleSearchResultWithStringBase(controls, searchResult);

		Object expectedResult = expectedObject;
		when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult);

		List list = tested.search(DEFAULT_BASE_STRING, "(ou=somevalue)", contextMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_String_SearchControls_ContextMapper() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsRecursive();

		Object expectedObject = new Object();
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		singleSearchResultWithStringBase(controls, searchResult);

		Object expectedResult = expectedObject;
		when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult);

		List list = tested.search(DEFAULT_BASE_STRING, "(ou=somevalue)", controls, contextMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

		assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_String_SearchControls_ContextMapper_ReturningObjFlagNotSet() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = new SearchControls();
		controls.setSearchScope(SearchControls.SUBTREE_SCOPE);

		SearchControls expectedControls = new SearchControls();
		expectedControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
		expectedControls.setReturningObjFlag(true);

		Object expectedObject = new Object();
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		singleSearchResultWithStringBase(expectedControls, searchResult);

		Object expectedResult = expectedObject;
		when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult);

		List list = tested.search(DEFAULT_BASE_STRING, "(ou=somevalue)", controls, contextMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_Name_SearchControls_ContextMapper() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsRecursive();

		Object expectedObject = new Object();
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		singleSearchResult(controls, searchResult);

		Object expectedResult = expectedObject;
		when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult);

		List list = tested.search(nameMock, "(ou=somevalue)", controls, contextMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_String_SearchControls_AttributesMapper() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsOneLevel();
		controls.setReturningObjFlag(false);

		BasicAttributes expectedAttributes = new BasicAttributes();
		SearchResult searchResult = new SearchResult("", null, expectedAttributes);

		singleSearchResultWithStringBase(controls, searchResult);

		Object expectedResult = new Object();
		when(attributesMapperMock.mapFromAttributes(expectedAttributes)).thenReturn(expectedResult);

		List list = tested.search(DEFAULT_BASE_STRING, "(ou=somevalue)", controls, attributesMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testSearch_Name_SearchControls_AttributesMapper() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsOneLevel();
		controls.setReturningObjFlag(false);

		BasicAttributes expectedAttributes = new BasicAttributes();
		SearchResult searchResult = new SearchResult("", null, expectedAttributes);

		singleSearchResult(controls, searchResult);

		Object expectedResult = new Object();
		when(attributesMapperMock.mapFromAttributes(expectedAttributes)).thenReturn(expectedResult);

		List list = tested.search(nameMock, "(ou=somevalue)", controls, attributesMapperMock);

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();

        assertThat(list).isNotNull();
		assertThat(list).hasSize(1);
		assertThat(list.get(0)).isSameAs(expectedResult);
	}

    @Test
	public void testModifyAttributes() throws Exception {
		expectGetReadWriteContext();

		ModificationItem[] mods = new ModificationItem[0];

		tested.modifyAttributes(nameMock, mods);

        verify(dirContextMock).modifyAttributes(nameMock, mods);
        verify(dirContextMock).close();
	}

    @Test
	public void testModifyAttributes_String() throws Exception {
		expectGetReadWriteContext();

		ModificationItem[] mods = new ModificationItem[0];

		tested.modifyAttributes(DEFAULT_BASE_STRING, mods);

        verify(dirContextMock).modifyAttributes(DEFAULT_BASE_STRING, mods);
        verify(dirContextMock).close();
	}

    @Test
	public void testModifyAttributes_NamingException() throws Exception {
		expectGetReadWriteContext();

		ModificationItem[] mods = new ModificationItem[0];

        javax.naming.LimitExceededException ne = new javax.naming.LimitExceededException();
        doThrow(ne).when(dirContextMock).modifyAttributes(nameMock, mods);

		try {
			tested.modifyAttributes(nameMock, mods);
			fail("LimitExceededException expected");
		}
		catch (LimitExceededException expected) {
			assertThat(true).isTrue();
		}

        verify(dirContextMock).close();
	}

    @Test
	public void testBind() throws Exception {
		expectGetReadWriteContext();

		Object expectedObject = new Object();
		BasicAttributes expectedAttributes = new BasicAttributes();

		tested.bind(nameMock, expectedObject, expectedAttributes);

        verify(dirContextMock).bind(nameMock, expectedObject, expectedAttributes);
        verify(dirContextMock).close();

    }

    @Test
	public void testBind_String() throws Exception {
		expectGetReadWriteContext();

		Object expectedObject = new Object();
		BasicAttributes expectedAttributes = new BasicAttributes();

		tested.bind(DEFAULT_BASE_STRING, expectedObject, expectedAttributes);

        verify(dirContextMock).bind(DEFAULT_BASE_STRING, expectedObject, expectedAttributes);
        verify(dirContextMock).close();
	}

    @Test
	public void testBind_NamingException() throws Exception {
		expectGetReadWriteContext();

		Object expectedObject = new Object();
		BasicAttributes expectedAttributes = new BasicAttributes();
        javax.naming.NameNotFoundException ne = new javax.naming.NameNotFoundException();
        doThrow(ne).when(dirContextMock).bind(nameMock, expectedObject, expectedAttributes);

		try {
			tested.bind(nameMock, expectedObject, expectedAttributes);
			fail("NameNotFoundException expected");
		}
		catch (NameNotFoundException expected) {
			assertThat(true).isTrue();
		}

        verify(dirContextMock).close();
    }

    @Test
    public void testBindWithContext() throws Exception {
		expectGetReadWriteContext();

		when(dirContextOperationsMock.getDn()).thenReturn(nameMock);
		when(dirContextOperationsMock.isUpdateMode()).thenReturn(false);

		tested.bind(dirContextOperationsMock);

        verify(dirContextMock).bind(nameMock, dirContextOperationsMock, null);
        verify(dirContextMock).close();
	}

    @Test
    public void testCreateWithIdSpecified() throws NamingException {
        expectGetReadWriteContext();

        Object expectedObject = new Object();
        LdapName expectedName = LdapUtils.newLdapName("ou=someOu");
        when(odmMock.getId(expectedObject)).thenReturn(expectedName);

        ArgumentCaptor<DirContextAdapter> ctxCaptor = ArgumentCaptor.forClass(DirContextAdapter.class);
        doNothing().when(odmMock).mapToLdapDataEntry(eq(expectedObject), ctxCaptor.capture());

        tested.create(expectedObject);

        verify(odmMock, never()).setId(expectedObject, expectedName);
        verify(dirContextMock).bind(expectedName, ctxCaptor.getValue(), null);
        verify(dirContextMock).close();
    }

    @Test
    public void testCreateWithCalculatedId() throws NamingException {
        expectGetReadWriteContext();

        Object expectedObject = new Object();
        LdapName expectedName = LdapUtils.newLdapName("ou=someOu");
        when(odmMock.getId(expectedObject)).thenReturn(null);
        when(odmMock.getCalculatedId(expectedObject)).thenReturn(expectedName);

        ArgumentCaptor<DirContextAdapter> ctxCaptor = ArgumentCaptor.forClass(DirContextAdapter.class);
        doNothing().when(odmMock).mapToLdapDataEntry(eq(expectedObject), ctxCaptor.capture());

        tested.create(expectedObject);

        verify(odmMock).setId(expectedObject, expectedName);
        verify(dirContextMock).bind(expectedName, ctxCaptor.getValue(), null);
        verify(dirContextMock).close();
    }

    @Test
    public void testCreateWithNoIdAvailableThrows() throws NamingException {
        Object expectedObject = new Object();
        when(odmMock.getId(expectedObject)).thenReturn(null);
        when(odmMock.getCalculatedId(expectedObject)).thenReturn(null);

        try {
            tested.create(expectedObject);
            fail("IllegalArgumentException expected");
        } catch (IllegalArgumentException expected) {
            assertThat(true).isTrue();
        }
    }

    @Test
    public void testUpdateWithIdSpecified() throws NamingException {
        when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock);
        when(contextSourceMock.getReadWriteContext()).thenReturn(dirContextMock);
        LdapName expectedName = LdapUtils.newLdapName("ou=someOu");

        ModificationItem[] expectedModificationItems = new ModificationItem[0];
        DirContextOperations ctxMock = mock(DirContextOperations.class);
        when(ctxMock.getDn()).thenReturn(expectedName);
        when(ctxMock.isUpdateMode()).thenReturn(true);
        when(ctxMock.getModificationItems()).thenReturn(expectedModificationItems);

        Object expectedObject = new Object();
        when(odmMock.getId(expectedObject)).thenReturn(expectedName);
        when(odmMock.getCalculatedId(expectedObject)).thenReturn(null);

        when(dirContextMock.lookup(expectedName)).thenReturn(ctxMock);

        tested.update(expectedObject);

        verify(odmMock, never()).setId(expectedObject, expectedName);
        verify(odmMock).mapToLdapDataEntry(expectedObject, ctxMock);
        verify(dirContextMock).modifyAttributes(expectedName, expectedModificationItems);

        verify(dirContextMock, times(2)).close();
    }

    @Test
    public void testUpdateWithIdCalculated() throws NamingException {
        when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock);
        when(contextSourceMock.getReadWriteContext()).thenReturn(dirContextMock);
        LdapName expectedName = LdapUtils.newLdapName("ou=someOu");

        ModificationItem[] expectedModificationItems = new ModificationItem[0];
        DirContextOperations ctxMock = mock(DirContextOperations.class);
        when(ctxMock.getDn()).thenReturn(expectedName);
        when(ctxMock.isUpdateMode()).thenReturn(true);
        when(ctxMock.getModificationItems()).thenReturn(expectedModificationItems);

        Object expectedObject = new Object();
        when(odmMock.getId(expectedObject)).thenReturn(null);
        when(odmMock.getCalculatedId(expectedObject)).thenReturn(expectedName);

        when(dirContextMock.lookup(expectedName)).thenReturn(ctxMock);

        tested.update(expectedObject);

        verify(odmMock).setId(expectedObject, expectedName);
        verify(odmMock).mapToLdapDataEntry(expectedObject, ctxMock);
        verify(dirContextMock).modifyAttributes(expectedName, expectedModificationItems);

        verify(dirContextMock, times(2)).close();
    }

    @Test
    public void testUpdateWithIdChanged() throws NamingException {
        Object expectedObject = new Object();

        when(contextSourceMock.getReadWriteContext()).thenReturn(dirContextMock, dirContextMock);
        LdapName expectedOriginalName = LdapUtils.newLdapName("ou=someOu");
        LdapName expectedNewName = LdapUtils.newLdapName("ou=someOtherOu");

        ArgumentCaptor<DirContextAdapter> ctxCaptor = ArgumentCaptor.forClass(DirContextAdapter.class);
        doNothing().when(odmMock).mapToLdapDataEntry(eq(expectedObject), ctxCaptor.capture());

        when(odmMock.getId(expectedObject)).thenReturn(expectedOriginalName);
        when(odmMock.getCalculatedId(expectedObject)).thenReturn(expectedNewName);

        tested.update(expectedObject);

        verify(odmMock).setId(expectedObject, expectedNewName);
        verify(dirContextMock).unbind(expectedOriginalName);
        verify(dirContextMock).bind(expectedNewName, ctxCaptor.getValue(), null);
        verify(dirContextMock, times(2)).close();
    }

    @Test
	public void testUnbind() throws Exception {
		expectGetReadWriteContext();

		tested.unbind(nameMock);

        verify(dirContextMock).unbind(nameMock);
        verify(dirContextMock).close();
	}

    @Test
	public void testUnbind_String() throws Exception {
		expectGetReadWriteContext();

		tested.unbind(DEFAULT_BASE_STRING);

        verify(dirContextMock).unbind(DEFAULT_BASE_STRING);
        verify(dirContextMock).close();
	}

    @Test
    public void testRebindWithContext() throws Exception {
		expectGetReadWriteContext();

		when(dirContextOperationsMock.getDn()).thenReturn(nameMock);
		when(dirContextOperationsMock.isUpdateMode()).thenReturn(false);

		tested.rebind(dirContextOperationsMock);

        verify(dirContextMock).rebind(nameMock, dirContextOperationsMock, null);
        verify(dirContextMock).close();
	}

    @Test
	public void testUnbindRecursive() throws Exception {
		expectGetReadWriteContext();

		when(namingEnumerationMock.hasMore()).thenReturn(true, false, false);
		Binding binding = new Binding("cn=Some name", null);
		when(namingEnumerationMock.next()).thenReturn(binding);

		LdapName listDn = LdapUtils.newLdapName(DEFAULT_BASE_STRING);
		when(dirContextMock.listBindings(listDn)).thenReturn(namingEnumerationMock);
		LdapName subListDn = LdapUtils.newLdapName("cn=Some name, o=example.com");
		when(dirContextMock.listBindings(subListDn)).thenReturn(namingEnumerationMock);

		tested.unbind(new CompositeName(DEFAULT_BASE_STRING), true);

        verify(dirContextMock).unbind(subListDn);
        verify(dirContextMock).unbind(listDn);
        verify(namingEnumerationMock, times(2)).close();
        verify(dirContextMock).close();
	}

    @Test
	public void testUnbindRecursive_String() throws Exception {
		expectGetReadWriteContext();

		when(namingEnumerationMock.hasMore()).thenReturn(true, false, false);
		Binding binding = new Binding("cn=Some name", null);
		when(namingEnumerationMock.next()).thenReturn(binding);

		LdapName listDn = LdapUtils.newLdapName(DEFAULT_BASE_STRING);
		when(dirContextMock.listBindings(listDn)).thenReturn(namingEnumerationMock);
		LdapName subListDn = LdapUtils.newLdapName("cn=Some name, o=example.com");
		when(dirContextMock.listBindings(subListDn)).thenReturn(namingEnumerationMock);

		tested.unbind(DEFAULT_BASE_STRING, true);

        verify(dirContextMock).unbind(subListDn);
        verify(dirContextMock).unbind(listDn);
        verify(namingEnumerationMock, times(2)).close();
        verify(dirContextMock).close();
	}

    @Test
	public void testRebind() throws Exception {
		expectGetReadWriteContext();

		Object expectedObject = new Object();
		BasicAttributes expectedAttributes = new BasicAttributes();

		tested.rebind(nameMock, expectedObject, expectedAttributes);

        verify(dirContextMock).rebind(nameMock, expectedObject, expectedAttributes);
        verify(dirContextMock).close();
	}

    @Test
	public void testRebind_String() throws Exception {
		expectGetReadWriteContext();

		Object expectedObject = new Object();
		BasicAttributes expectedAttributes = new BasicAttributes();

		tested.rebind(DEFAULT_BASE_STRING, expectedObject, expectedAttributes);

        verify(dirContextMock).rebind(DEFAULT_BASE_STRING, expectedObject, expectedAttributes);
        verify(dirContextMock).close();
	}

    @Test
	public void testUnbind_NamingException() throws Exception {
		expectGetReadWriteContext();

        javax.naming.NameNotFoundException ne = new javax.naming.NameNotFoundException();
        doThrow(ne).when(dirContextMock).unbind(nameMock);

		try {
			tested.unbind(nameMock);
			fail("NameNotFoundException expected");
		}
		catch (NameNotFoundException expected) {
			assertThat(true).isTrue();
		}

        verify(dirContextMock).close();
	}

    @Test
	public void testExecuteReadOnly() throws Exception {
		expectGetReadOnlyContext();

		Object object = new Object();
		when(contextExecutorMock.executeWithContext(dirContextMock)).thenReturn(object);

		Object result = tested.executeReadOnly(contextExecutorMock);

        verify(dirContextMock).close();

		assertThat(result).isSameAs(object);
	}

    @Test
	public void testExecuteReadOnly_NamingException() throws Exception {
		expectGetReadOnlyContext();

		javax.naming.NameNotFoundException ne = new javax.naming.NameNotFoundException();
		when(contextExecutorMock.executeWithContext(dirContextMock)).thenThrow(ne);

		try {
			tested.executeReadOnly(contextExecutorMock);
			fail("NameNotFoundException expected");
		}
		catch (NameNotFoundException expected) {
			assertThat(true).isTrue();
		}

        verify(dirContextMock).close();
	}

    @Test
	public void testExecuteReadWrite() throws Exception {
		expectGetReadWriteContext();

		Object object = new Object();
		when(contextExecutorMock.executeWithContext(dirContextMock)).thenReturn(object);

		Object result = tested.executeReadWrite(contextExecutorMock);

        verify(dirContextMock).close();

        assertThat(result).isSameAs(object);
	}

    @Test
	public void testExecuteReadWrite_NamingException() throws Exception {
		expectGetReadWriteContext();

		javax.naming.NameNotFoundException ne = new javax.naming.NameNotFoundException();
		when(contextExecutorMock.executeWithContext(dirContextMock)).thenThrow(ne);

		try {
			tested.executeReadWrite(contextExecutorMock);
			fail("NameNotFoundException expected");
		}
		catch (NameNotFoundException expected) {
			assertThat(true).isTrue();
		}

        verify(dirContextMock).close();
	}

    @Test
	public void testDoSearch_DirContextProcessor() throws Exception {
		expectGetReadOnlyContext();

		SearchResult searchResult = new SearchResult(null, null, null);

		when(searchExecutorMock.executeSearch(dirContextMock)).thenReturn(namingEnumerationMock);

		when(namingEnumerationMock.hasMore()).thenReturn(true, false);
		when(namingEnumerationMock.next()).thenReturn(searchResult);

		tested.search(searchExecutorMock, handlerMock, dirContextProcessorMock);

        verify(dirContextProcessorMock).preProcess(dirContextMock);
        verify(dirContextProcessorMock).postProcess(dirContextMock);
        verify(handlerMock).handleNameClassPair(searchResult);
        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();
	}

    @Test
	public void testDoSearch_DirContextProcessor_NamingException() throws Exception {
		expectGetReadOnlyContext();

		javax.naming.LimitExceededException ne = new javax.naming.LimitExceededException();
		when(searchExecutorMock.executeSearch(dirContextMock)).thenThrow(ne);

		try {
			tested.search(searchExecutorMock, handlerMock, dirContextProcessorMock);
			fail("LimitExceededException expected");
		}
		catch (LimitExceededException expected) {
			assertThat(true).isTrue();
		}

        verify(dirContextProcessorMock).preProcess(dirContextMock);
        verify(dirContextProcessorMock).postProcess(dirContextMock);
        verify(dirContextMock).close();
	}

    @Test
	public void testDoSearch() throws Exception {
		expectGetReadOnlyContext();

		SearchResult searchResult = new SearchResult(null, null, null);

		when(searchExecutorMock.executeSearch(dirContextMock)).thenReturn(namingEnumerationMock);

		when(namingEnumerationMock.hasMore()).thenReturn(true, false);
		when(namingEnumerationMock.next()).thenReturn(searchResult);

		tested.search(searchExecutorMock, handlerMock);

        verify(handlerMock).handleNameClassPair(searchResult);
        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();
	}

    @Test
	public void testDoSearch_NamingException() throws Exception {
		expectGetReadOnlyContext();

		javax.naming.LimitExceededException ne = new javax.naming.LimitExceededException();
		when(searchExecutorMock.executeSearch(dirContextMock)).thenThrow(ne);

		try {
			tested.search(searchExecutorMock, handlerMock);
			fail("LimitExceededException expected");
		}
		catch (LimitExceededException expected) {
			assertThat(true).isTrue();
		}

        verify(dirContextMock).close();
	}

    @Test
	public void testDoSearch_NamingException_NamingEnumeration() throws Exception {
		expectGetReadOnlyContext();

		when(searchExecutorMock.executeSearch(dirContextMock)).thenReturn(namingEnumerationMock);

		javax.naming.LimitExceededException ne = new javax.naming.LimitExceededException();
		when(namingEnumerationMock.hasMore()).thenThrow(ne);

		try {
			tested.search(searchExecutorMock, handlerMock);
			fail("LimitExceededException expected");
		}
		catch (LimitExceededException expected) {
			assertThat(true).isTrue();
		}

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();
	}

    @Test
	public void testDoSearch_NameNotFoundException() throws Exception {
		expectGetReadOnlyContext();

		when(searchExecutorMock.executeSearch(dirContextMock)).thenThrow(new javax.naming.NameNotFoundException());

		try {
			tested.search(searchExecutorMock, handlerMock);
			fail("NameNotFoundException expected");
		}
		catch (NameNotFoundException expected) {
			assertThat(true).isTrue();
		}

        verify(dirContextMock).close();
	}

    @Test
	public void testSearch_PartialResult_IgnoreNotSet() throws Exception {
		expectGetReadOnlyContext();

		javax.naming.PartialResultException ex = new javax.naming.PartialResultException();
		when(searchExecutorMock.executeSearch(dirContextMock)).thenThrow(ex);

		try {
			tested.search(searchExecutorMock, handlerMock, dirContextProcessorMock);
			fail("PartialResultException expected");
		}
		catch (PartialResultException expected) {
			assertThat(true).isTrue();
		}

        verify(dirContextProcessorMock).preProcess(dirContextMock);
        verify(dirContextProcessorMock).postProcess(dirContextMock);
        verify(dirContextMock).close();
	}

    @Test
	public void testSearch_PartialResult_IgnoreSet() throws Exception {
		tested.setIgnorePartialResultException(true);

		expectGetReadOnlyContext();

		when(searchExecutorMock.executeSearch(dirContextMock)).thenThrow(new javax.naming.PartialResultException());

		tested.search(searchExecutorMock, handlerMock, dirContextProcessorMock);

        verify(dirContextProcessorMock).preProcess(dirContextMock);
        verify(dirContextProcessorMock).postProcess(dirContextMock);
        verify(dirContextMock).close();
	}

    @Test
	public void testLookupContextWithName() {
		final DirContextAdapter expectedResult = new DirContextAdapter();

        final LdapName expectedName = LdapUtils.emptyLdapName();
        LdapTemplate tested = new LdapTemplate() {
			public Object lookup(Name dn) {
				assertThat(dn).isSameAs(dn);
				return expectedResult;
			}
		};

		DirContextOperations result = tested.lookupContext(expectedName);
		assertThat(result).isSameAs(expectedResult);

	}

    @Test
	public void testLookupContextWithString() {
		final DirContextAdapter expectedResult = new DirContextAdapter();
		final String expectedName = "cn=John Doe";

		LdapTemplate tested = new LdapTemplate() {
			public Object lookup(String dn) {
				assertThat(dn).isSameAs(expectedName);
				return expectedResult;
			}
		};

		DirContextOperations result = tested.lookupContext(expectedName);
		assertThat(result).isSameAs(expectedResult);
	}

    @Test
	public void testModifyAttributesWithDirContextOperations() throws Exception {
		final ModificationItem[] expectedModifications = new ModificationItem[0];

        final LdapName epectedDn = LdapUtils.emptyLdapName();
        when(dirContextOperationsMock.getDn()).thenReturn(epectedDn);
		when(dirContextOperationsMock.isUpdateMode()).thenReturn(true);
		when(dirContextOperationsMock.getModificationItems()).thenReturn(expectedModifications);

		LdapTemplate tested = new LdapTemplate() {
			public void modifyAttributes(Name dn, ModificationItem[] mods) {
				assertThat(dn).isSameAs(epectedDn);
				assertThat(mods).isSameAs(expectedModifications);
			}
		};

		tested.modifyAttributes(dirContextOperationsMock);
	}

    @Test
	public void testModifyAttributesWithDirContextOperationsNotInitializedDn() throws Exception {

		when(dirContextOperationsMock.getDn()).thenReturn(LdapUtils.emptyLdapName());
		when(dirContextOperationsMock.isUpdateMode()).thenReturn(false);

		LdapTemplate tested = new LdapTemplate() {
			public void modifyAttributes(Name dn, ModificationItem[] mods) {
				fail("The call to the base modifyAttributes should not have occured.");
			}
		};

		try {
			tested.modifyAttributes(dirContextOperationsMock);
			fail("IllegalStateException expected");
		}
		catch (IllegalStateException expected) {
			assertThat(true).isTrue();
		}
	}

    @Test
	public void testModifyAttributesWithDirContextOperationsNotInitializedInUpdateMode() throws Exception {
		when(dirContextOperationsMock.getDn()).thenReturn(null);

		LdapTemplate tested = new LdapTemplate() {
			public void modifyAttributes(Name dn, ModificationItem[] mods) {
				fail("The call to the base modifyAttributes should not have occured.");
			}
		};

		try {
			tested.modifyAttributes(dirContextOperationsMock);
			fail("IllegalStateException expected");
		}
		catch (IllegalStateException expected) {
			assertThat(true).isTrue();
		}
	}

    @Test
	public void testSearchForObject() throws Exception {
		expectGetReadOnlyContext();

		Object expectedObject = new Object();
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		singleSearchResult(searchControlsRecursive(), searchResult);

		Object expectedResult = expectedObject;
		when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult);

		Object result = tested.searchForObject(nameMock, "(ou=somevalue)", contextMapperMock);

        verify(dirContextMock).close();

        assertThat(result).isNotNull();
		assertThat(result).isSameAs(expectedResult);
	}

    @Test
	public void testSearchForObjectWithMultipleResults() throws Exception {
		expectGetReadOnlyContext();

		SearchControls controls = searchControlsRecursive();

		Object expectedObject = new Object();
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		when(dirContextMock.search(
                eq(nameMock),
                eq("(ou=somevalue)"),
                argThat(new SearchControlsMatcher(controls)))).thenReturn(namingEnumerationMock);

		when(namingEnumerationMock.hasMore()).thenReturn(true, true, false);
		when(namingEnumerationMock.next()).thenReturn(searchResult, searchResult);

		Object expectedResult = expectedObject;
		when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult);
		when(contextMapperMock.mapFromContext(expectedObject)).thenReturn(expectedResult);

		try {
			tested.searchForObject(nameMock, "(ou=somevalue)", contextMapperMock);
			fail("IncorrectResultSizeDataAccessException expected");
		}
		catch (IncorrectResultSizeDataAccessException expected) {
			assertThat(true).isTrue();
		}

        verify(namingEnumerationMock).close();
        verify(dirContextMock).close();
	}

    @Test
	public void testSearchForObjectWithNoResults() throws Exception {
		expectGetReadOnlyContext();

		noSearchResults(searchControlsRecursive());

		try {
			tested.searchForObject(nameMock, "(ou=somevalue)", contextMapperMock);
			fail("EmptyResultDataAccessException expected");
		}
		catch (EmptyResultDataAccessException expected) {
			assertThat(true).isTrue();
		}

        verify(dirContextMock).close();
    }

    @Test
	public void testAuthenticateWithSingleUserFoundShouldBeSuccessful() throws Exception {
		when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock);

		Object expectedObject = new DirContextAdapter(new BasicAttributes(), LdapUtils.newLdapName("cn=john doe"),
                LdapUtils.newLdapName("dc=jayway, dc=se"));
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		singleSearchResult(searchControlsRecursive(), searchResult);

		when(contextSourceMock.getContext("cn=john doe,dc=jayway,dc=se", "password"))
                .thenReturn(authenticatedContextMock);
		entryContextCallbackMock.executeWithContext(authenticatedContextMock, new LdapEntryIdentification(
                LdapUtils.newLdapName("cn=john doe,dc=jayway,dc=se"), LdapUtils.newLdapName("cn=john doe")));

		boolean result = tested.authenticate(nameMock, "(ou=somevalue)", "password", entryContextCallbackMock);

        verify(authenticatedContextMock).close();
        verify(dirContextMock).close();

		assertThat(result).isTrue();
	}

    @Test
	public void testAuthenticateWithTwoUsersFoundShouldThrowException() throws Exception {
		when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock);

		Object expectedObject = new DirContextAdapter(new BasicAttributes(), LdapUtils.newLdapName("cn=john doe"),
                LdapUtils.newLdapName("dc=jayway, dc=se"));
		SearchResult searchResult1 = new SearchResult("", expectedObject, new BasicAttributes());
		SearchResult searchResult2 = new SearchResult("", expectedObject, new BasicAttributes());

		setupSearchResults(searchControlsRecursive(), new SearchResult[] { searchResult1, searchResult2 });

		try {
			tested.authenticate(nameMock, "(ou=somevalue)", "password", entryContextCallbackMock);
			fail("IncorrectResultSizeDataAccessException expected");
		}
		catch (IncorrectResultSizeDataAccessException expected) {
			// expected
		}

        verify(dirContextMock).close();
	}

    @Test
	public void testAuthenticateWhenNoUserWasFoundShouldFail() throws Exception {
		when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock);

		noSearchResults(searchControlsRecursive());

		boolean result = tested.authenticate(nameMock, "(ou=somevalue)", "password", entryContextCallbackMock);

        verify(dirContextMock).close();

        assertThat(result).isFalse();
	}

	@Test
	@SuppressWarnings("unchecked")
	public void testAuthenticateQueryPasswordMapperWhenNoUserWasFoundShouldThrowEmptyResult() throws Exception {

		when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock);

		 when(dirContextMock.search(
					any(Name.class),
					any(String.class),
					any(SearchControls.class))).thenReturn(namingEnumerationMock);

			when(namingEnumerationMock.hasMore()).thenReturn(false);

		try {
			tested.authenticate(query, "", authContextMapperMock);
			fail("Expected Exception");
		}catch(EmptyResultDataAccessException success) {}
		verify(dirContextMock).close();
	}

	@Test
	@SuppressWarnings("unchecked")
	public void testAuthenticateQueryPasswordWhenNoUserWasFoundShouldThrowEmptyResult() throws Exception {

		when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock);

		 when(dirContextMock.search(
					any(Name.class),
					any(String.class),
					any(SearchControls.class))).thenReturn(namingEnumerationMock);

			when(namingEnumerationMock.hasMore()).thenReturn(false);

		try {
			tested.authenticate(query, "");
			fail("Expected Exception");
		}catch(EmptyResultDataAccessException success) {}
		verify(dirContextMock).close();
	}

    @Test
	public void testAuthenticateWithFailedAuthenticationShouldFail() throws Exception {
		when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock);

		Object expectedObject = new DirContextAdapter(new BasicAttributes(), LdapUtils.newLdapName("cn=john doe"),
                LdapUtils.newLdapName("dc=jayway, dc=se"));
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		singleSearchResult(searchControlsRecursive(), searchResult);

		when(contextSourceMock.getContext("cn=john doe,dc=jayway,dc=se", "password"))
                .thenThrow(new UncategorizedLdapException("Authentication failed"));

		boolean result = tested.authenticate(nameMock, "(ou=somevalue)", "password", entryContextCallbackMock);

        verify(dirContextMock).close();

        assertThat(result).isFalse();
	}

    @Test
	public void testAuthenticateWithErrorInCallbackShouldFail() throws Exception {
		when(contextSourceMock.getReadOnlyContext()).thenReturn(dirContextMock);

		Object expectedObject = new DirContextAdapter(new BasicAttributes(), LdapUtils.newLdapName("cn=john doe"),
				LdapUtils.newLdapName("dc=jayway, dc=se"));
		SearchResult searchResult = new SearchResult("", expectedObject, new BasicAttributes());

		singleSearchResult(searchControlsRecursive(), searchResult);

		when(contextSourceMock.getContext("cn=john doe,dc=jayway,dc=se", "password"))
                .thenReturn(authenticatedContextMock);
        doThrow(new UncategorizedLdapException("Authentication failed")).when(entryContextCallbackMock)
                .executeWithContext(authenticatedContextMock,
                        new LdapEntryIdentification(
                                LdapUtils.newLdapName("cn=john doe,dc=jayway,dc=se"), LdapUtils.newLdapName("cn=john doe")));

		boolean result = tested.authenticate(nameMock, "(ou=somevalue)", "password", entryContextCallbackMock);

        verify(authenticatedContextMock).close();
        verify(dirContextMock).close();

        assertThat(result).isFalse();
	}

	private void noSearchResults(SearchControls controls) throws Exception {
        when(dirContextMock.search(
                eq(nameMock),
                eq("(ou=somevalue)"),
                argThat(new SearchControlsMatcher(controls)))).thenReturn(namingEnumerationMock);

        when(namingEnumerationMock.hasMore()).thenReturn(false);
    }

	private void singleSearchResult(SearchControls controls, SearchResult searchResult) throws Exception {
		setupSearchResults(controls, new SearchResult[] { searchResult });
	}

	private void setupSearchResults(SearchControls controls, SearchResult[] searchResults) throws Exception {
        when(dirContextMock.search(
                eq(nameMock),
                eq("(ou=somevalue)"),
                argThat(new SearchControlsMatcher(controls)))).thenReturn(namingEnumerationMock);

        if(searchResults.length == 1) {
            when(namingEnumerationMock.hasMore()).thenReturn(true, false);
            when(namingEnumerationMock.next()).thenReturn(searchResults[0]);
        } else if(searchResults.length ==2) {
            when(namingEnumerationMock.hasMore()).thenReturn(true, true, false);
            when(namingEnumerationMock.next()).thenReturn(searchResults[0], searchResults[1]);
        } else {
            throw new IllegalArgumentException("Cannot handle " + searchResults.length + " search results");
        }
	}

	private void singleSearchResultWithStringBase(SearchControls controls, SearchResult searchResult)
			throws Exception {
        when(dirContextMock.search(
                eq(DEFAULT_BASE_STRING),
                eq("(ou=somevalue)"),
                argThat(new SearchControlsMatcher(controls)))).thenReturn(namingEnumerationMock);

        when(namingEnumerationMock.hasMore()).thenReturn(true, false);
        when(namingEnumerationMock.next()).thenReturn(searchResult);
	}

	private SearchControls searchControlsRecursive() {
		SearchControls controls = new SearchControls();
		controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
		controls.setReturningObjFlag(true);
		return controls;
	}

	private SearchControls searchControlsOneLevel() {
		SearchControls controls = new SearchControls();
		controls.setSearchScope(SearchControls.ONELEVEL_SCOPE);
		controls.setReturningObjFlag(true);
		return controls;
	}

    private static class SearchControlsMatcher extends BaseMatcher<SearchControls> {
        private final SearchControls controls;

        public SearchControlsMatcher(SearchControls controls) {
            this.controls = controls;
        }

        @Override
        public boolean matches(Object item) {
            if (item instanceof SearchControls) {
                SearchControls s1 = (SearchControls) item;

                return controls.getSearchScope() == s1.getSearchScope()
                        && controls.getReturningObjFlag() == s1.getReturningObjFlag()
                        && controls.getDerefLinkFlag() == s1.getDerefLinkFlag()
                        && controls.getCountLimit() == s1.getCountLimit()
                        && controls.getTimeLimit() == s1.getTimeLimit()
                        && controls.getReturningAttributes() == s1.getReturningAttributes();
            }
            else {
                throw new IllegalArgumentException();
            }
        }

        @Override
        public void describeTo(Description description) {
            description.appendText("SearchControls matches");
        }
    }
}