DirectoryServerExtension.java

/*
 * Copyright 2021-2025 Brian Thomas Matthews
 *
 * 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 com.buralotech.oss.ldapunit;

import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldif.LDIFException;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;

import java.io.IOException;

/**
 * JUnit 5 (Jupiter) extension that will start an embedded directory server before the test method execution and
 * stop the embedded directory server when the test method completes.
 *
 * @author <a href="mailto:bmatthews68@gmail.com">Brian Matthews</a>
 * @since 2.0.0
 */
public class DirectoryServerExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback,
        ParameterResolver {

    /**
     * The name of the property used to cache the reference to the embedded directory server.
     */
    private static final String SERVER = "server";

    /**
     * This callback is invoked before the test method is executed and is responsible for starting the embedded
     * directory server.
     *
     * @param extensionContext – the extension context for the Executable about to be invoked; never {@code null}.
     */
    @Override
    public void beforeTestExecution(final ExtensionContext extensionContext) {
        final DirectoryServerConfiguration annotation = getAnnotation(extensionContext);
        if (annotation != null) {
            try {
                final InMemoryDirectoryServer server = DirectoryServerUtils.startServer(annotation);
                getStore(extensionContext).put(SERVER, server);
            } catch (final LDIFException | LDAPException | IOException e) {
                throw new AssertionError("Failed to launch embedded Directory Server", e);
            }
        }
    }

    /**
     * This callback is invoked after the test method is executed and is responsible for stopping the embedded
     * directory server.
     *
     * @param extensionContext – the extension context for the Executable about to be invoked; never {@code null}.
     */
    @Override
    public void afterTestExecution(final ExtensionContext extensionContext) {
        final Store store = getStore(extensionContext);
        final InMemoryDirectoryServer server = store.get(SERVER, InMemoryDirectoryServer.class);
        if (server != null) {
            DirectoryServerUtils.stopServer(server);
        }
    }

    /**
     * Get teh context storage for the method invocation.
     *
     * @param extensionContext – the extension context for the Executable about to be invoked; never {@code null}.
     * @return The context store.
     */
    private Store getStore(final ExtensionContext extensionContext) {
        final Namespace namespace = Namespace.create(
                DirectoryServerExtension.class,
                extensionContext.getRequiredTestMethod());
        return extensionContext.getStore(namespace);
    }

    /**
     * Locate the annotation that specifies the configuration for the embedded directory server. The annotation is
     * sought on the test method declaration before falling back to check the test class.
     *
     * @param extensionContext – the extension context for the Executable about to be invoked; never {@code null}.
     * @return The {@code DirectoryServerConfiguration} annotation if found. Otherwise, {@code null}.
     */
    private DirectoryServerConfiguration getAnnotation(final ExtensionContext extensionContext) {
        final DirectoryServerConfiguration annotation = extensionContext.getRequiredTestMethod()
                .getAnnotation(DirectoryServerConfiguration.class);
        if (annotation == null) {
            return extensionContext.getRequiredTestClass().getAnnotation(DirectoryServerConfiguration.class);
        } else {
            return annotation;
        }
    }

    /**
     * Check the parameter type is {@link DirectoryTester}.
     *
     * @param parameterContext The context for the parameter for which an argument should be resolved;
     *                         never {@code null}.
     * @param extensionContext The extension context for the Executable about to be invoked; never {@code null}.
     * @return {@code true} if the parameter type is {@link DirectoryTester}. Otherwise, {@code false}.
     */
    @Override
    public boolean supportsParameter(final ParameterContext parameterContext,
                                     final ExtensionContext extensionContext)
            throws ParameterResolutionException {
        return DirectoryTester.class.equals(parameterContext.getParameter().getType());
    }

    /**
     * Resolve {@link DirectoryTester} parameters.
     *
     * @param parameterContext The context for the parameter for which an argument should be resolved;
     *                         never {@code null}.
     * @param extensionContext –The extension context for the Executable about to be invoked; never {@code null}.
     * @return The resolved parameter.
     * @throws ParameterResolutionException If a connection to the directory server could not be established.
     */
    @Override
    public Object resolveParameter(final ParameterContext parameterContext,
                                   final ExtensionContext extensionContext)
            throws ParameterResolutionException {
        final InMemoryDirectoryServer server = getStore(extensionContext).get(SERVER, InMemoryDirectoryServer.class);
        if (server != null) {
            try {
                return new DirectoryTester(server.getConnection());
            } catch (final LDAPException e) {
                throw new ParameterResolutionException("Cannot connect to directory server", e);
            }
        }
        return null;
    }
}