EmbeddedLdapServer builder

Closes #938

Signed-off-by: Emanuel Trandafir <emanueltrandafir1993@gmail.com>
Signed-off-by: etrandafir93 <emanueltrandafir1993@gmail.com>
This commit is contained in:
Emanuel Trandafir
2025-02-17 09:45:26 +02:00
committed by Josh Cummings
parent f154e64760
commit 8d44ee31a8
5 changed files with 230 additions and 27 deletions

View File

@@ -182,17 +182,19 @@ testCompile "com.unboundid:unboundid-ldapsdk:3.1.1"
The following bean definition creates an embedded LDAP server:
====
[source,xml]
[source,java]
----
<bean id="embeddedLdapServer" class="org.springframework.ldap.test.unboundid.EmbeddedLdapServerFactoryBean">
<property name="partitionName" value="example"/>
<property name="partitionSuffix" value="dc=261consulting,dc=com" />
<property name="port" value="9321" />
</bean>
@Bean
EmbeddedLdapServer embeddedLdapServer() {
return EmbeddedLdapServer.withPartitionSuffix("dc=jayway,dc=se")
.partitionName("jayway")
.port(18881)
.configurationCustomizer((config) -> config.setCodeLogDetails(tempLogFile, true))
.build();
}
----
====
`spring-ldap-test` provides a way to populate the LDAP server by using `org.springframework.ldap.test.unboundid.LdifPopulator`. To use it, create a bean similar to the following:
Alternatively, you can use the `org.springframework.ldap.test.unboundid.LdifPopulator` to create and populate the LDAP server. To use it, create a bean similar to the following:
====
[source,xml]

View File

@@ -16,6 +16,13 @@
package org.springframework.ldap.test.unboundid;
import java.util.List;
import java.util.function.Consumer;
import javax.naming.InvalidNameException;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
@@ -24,6 +31,7 @@ import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* Helper class for embedded Unboundid ldap server.
@@ -46,26 +54,29 @@ public final class EmbeddedLdapServer implements AutoCloseable {
}
/**
* Creates and starts new embedded LDAP server.
* Creates a new {@link Builder} with a given partition suffix.
*
* @since 3.3
*/
public static Builder withPartitionSuffix(String partitionSuffix) {
return new Builder(partitionSuffix);
}
/**
* Creates and starts new embedded LDAP server.
* @deprecated Use the builder pattern exposed via
* {@link #withPartitionSuffix(String)} instead.
*/
@Deprecated(since = "3.3")
public static EmbeddedLdapServer newEmbeddedServer(String defaultPartitionName, String defaultPartitionSuffix,
int port) throws Exception {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(defaultPartitionSuffix);
config.addAdditionalBindCredentials("uid=admin,ou=system", "secret");
int port) {
EmbeddedLdapServer server = EmbeddedLdapServer.withPartitionSuffix(defaultPartitionSuffix)
.partitionName(defaultPartitionName)
.port(port)
.build();
config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("LDAP", port));
config.setEnforceSingleStructuralObjectClass(false);
config.setEnforceAttributeSyntaxCompliance(true);
Entry entry = new Entry(new DN(defaultPartitionSuffix));
entry.addAttribute("objectClass", "top", "domain", "extensibleObject");
entry.addAttribute("dc", defaultPartitionName);
InMemoryDirectoryServer directoryServer = new InMemoryDirectoryServer(config);
directoryServer.add(entry);
directoryServer.startListening();
return new EmbeddedLdapServer(directoryServer);
server.start();
return server;
}
/**
@@ -102,4 +113,119 @@ public final class EmbeddedLdapServer implements AutoCloseable {
this.directoryServer.shutDown(true);
}
/**
* Helper class for embedded Unboundid ldap server.
*
* @author Emanuel Trandafir
* @since 3.3
*/
public static final class Builder {
private final String partitionSuffix;
private String partitionName;
private int port = 0;
private Consumer<InMemoryDirectoryServerConfig> configurationCustomizer = (__) -> {
};
private Builder(String partitionSuffix) {
this.partitionSuffix = partitionSuffix;
this.partitionName = leftMostElement(partitionSuffix);
}
/**
* Sets the port for the embedded LDAP server.
* @param port the port for the embedded LDAP server. Defaults to 0 in which case
* the server should automatically choose an available port.
* @return this {@link Builder} instance.
*/
public Builder port(int port) {
this.port = port;
return this;
}
/**
* Sets a customizer for the {@link InMemoryDirectoryServerConfig}.
* @param configurationCustomizer a {@link Consumer} function that will be applied
* to the {@link InMemoryDirectoryServerConfig} before creating the
* {@link InMemoryDirectoryServer}. The default values, it a Consumer function
* that does nothing: (config) -> {}
* @return this {@link Builder} instance.
*/
public Builder configurationCustomizer(Consumer<InMemoryDirectoryServerConfig> configurationCustomizer) {
this.configurationCustomizer = configurationCustomizer;
return this;
}
/**
* Sets the partition name for the embedded LDAP server.
* @param partitionName the partition name for the embedded LDAP server. Defaults
* to the left most element of the partition suffix.
* @return this {@link Builder} instance.
*/
public Builder partitionName(String partitionName) {
this.partitionName = partitionName;
return this;
}
/**
* Builds and returns a {@link EmbeddedLdapServer}.
* <p>
* In order to start the server, you should call
* {@link EmbeddedLdapServer#start()}.
* @return a new {@link EmbeddedLdapServer}.
*/
public EmbeddedLdapServer build() {
try {
InMemoryDirectoryServerConfig config = inMemoryDirectoryServerConfig(this.partitionSuffix, this.port);
this.configurationCustomizer.accept(config);
Entry entry = ldapEntry(this.partitionName, this.partitionSuffix);
InMemoryDirectoryServer directoryServer = inMemoryDirectoryServer(config, entry);
return new EmbeddedLdapServer(directoryServer);
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
}
static String leftMostElement(String partitionSuffix) {
try {
List<Rdn> rdns = new LdapName(partitionSuffix).getRdns();
return CollectionUtils.lastElement(rdns).getValue().toString();
}
catch (InvalidNameException ex) {
throw new RuntimeException(ex);
}
}
private static InMemoryDirectoryServerConfig inMemoryDirectoryServerConfig(String partitionSuffix, int port)
throws LDAPException {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(partitionSuffix);
config.addAdditionalBindCredentials("uid=admin,ou=system", "secret");
config.setListenerConfigs(InMemoryListenerConfig.createLDAPConfig("LDAP", port));
config.setEnforceSingleStructuralObjectClass(false);
config.setEnforceAttributeSyntaxCompliance(true);
return config;
}
private static Entry ldapEntry(String defaultPartitionName, String defaultPartitionSuffix)
throws LDAPException {
Entry entry = new Entry(new DN(defaultPartitionSuffix));
entry.addAttribute("objectClass", "top", "domain", "extensibleObject");
entry.addAttribute("dc", defaultPartitionName);
return entry;
}
private static InMemoryDirectoryServer inMemoryDirectoryServer(InMemoryDirectoryServerConfig config,
Entry entry) throws LDAPException {
InMemoryDirectoryServer directoryServer = new InMemoryDirectoryServer(config);
directoryServer.add(entry);
return directoryServer;
}
}
}

View File

@@ -48,7 +48,13 @@ public class EmbeddedLdapServerFactoryBean extends AbstractFactoryBean<EmbeddedL
@Override
protected EmbeddedLdapServer createInstance() throws Exception {
return EmbeddedLdapServer.newEmbeddedServer(this.partitionName, this.partitionSuffix, this.port);
EmbeddedLdapServer server = EmbeddedLdapServer.withPartitionSuffix(this.partitionSuffix)
.partitionName(this.partitionName)
.port(this.port)
.build();
server.start();
return server;
}
@Override

View File

@@ -78,7 +78,12 @@ public final class LdapTestUtils {
}
try {
embeddedServer = EmbeddedLdapServer.newEmbeddedServer(defaultPartitionName, defaultPartitionSuffix, port);
embeddedServer = EmbeddedLdapServer.withPartitionSuffix(defaultPartitionSuffix)
.partitionName(defaultPartitionName)
.port(port)
.build();
embeddedServer.start();
}
catch (Exception ex) {
throw new UncategorizedLdapException("Failed to start embedded server", ex);

View File

@@ -19,12 +19,22 @@ package org.springframework.ldap.test.unboundid;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import org.junit.Test;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.ldap.query.LdapQueryBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -97,6 +107,50 @@ public class EmbeddedLdapServerTests {
}
}
@Test
public void shouldBuildButNotStartTheServer() throws IOException {
int port = getFreePort();
EmbeddedLdapServer.withPartitionSuffix("dc=jayway,dc=se").port(port).build();
assertThat(isPortOpen(port)).isFalse();
}
@Test
public void shouldBuildTheServerWithCustomPort() throws IOException {
int port = getFreePort();
EmbeddedLdapServer.Builder serverBuilder = EmbeddedLdapServer.withPartitionSuffix("dc=jayway,dc=se").port(port);
try (EmbeddedLdapServer server = serverBuilder.build()) {
server.start();
assertThat(isPortOpen(port)).isTrue();
}
assertThat(isPortOpen(port)).isFalse();
}
@Test
public void shouldBuildLdapServerAndApplyCustomConfiguration() throws IOException {
int port = getFreePort();
String tempLogFile = Files.createTempFile("ldap-log-", ".txt").toAbsolutePath().toString();
EmbeddedLdapServer.Builder serverBuilder = EmbeddedLdapServer.withPartitionSuffix("dc=jayway,dc=se")
.port(port)
.configurationCustomizer((config) -> config.setCodeLogDetails(tempLogFile, true));
try (EmbeddedLdapServer server = serverBuilder.build()) {
server.start();
ldapTemplate("dc=jayway,dc=se", port).search(LdapQueryBuilder.query().where("objectclass").is("person"),
new AttributesMapper<>() {
public String mapFromAttributes(Attributes attrs) throws NamingException {
return (String) attrs.get("cn").get();
}
});
}
assertThat(Path.of(tempLogFile))
.as("Applying the custom configuration should create a log file and populate it with the request")
.isNotEmptyFile();
}
static boolean isPortOpen(int port) {
try (Socket ignored = new Socket("localhost", port)) {
return true;
@@ -112,4 +166,14 @@ public class EmbeddedLdapServerTests {
}
}
static LdapTemplate ldapTemplate(String base, int port) {
LdapContextSource ctx = new LdapContextSource();
ctx.setBase(base);
ctx.setUrl("ldap://127.0.0.1:" + port);
ctx.setUserDn("uid=admin,ou=system");
ctx.setPassword("secret");
ctx.afterPropertiesSet();
return new LdapTemplate(ctx);
}
}