Skip to content

Commit

Permalink
Add support for JUnit 5 extensions (zapodot#57)
Browse files Browse the repository at this point in the history
  • Loading branch information
bjansen committed Oct 28, 2020
1 parent d43da04 commit 83625a6
Show file tree
Hide file tree
Showing 14 changed files with 791 additions and 426 deletions.
22 changes: 20 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# embedded-ldap-junit
[![Build Status](https://travis-ci.org/zapodot/embedded-ldap-junit.svg?branch=master)](https://travis-ci.org/zapodot/embedded-ldap-junit) [![Coverage Status](https://coveralls.io/repos/zapodot/embedded-ldap-junit/badge.svg)](https://coveralls.io/r/zapodot/embedded-ldap-junit) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.zapodot/embedded-ldap-junit/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.zapodot/embedded-ldap-junit) [![Libraries.io for GitHub](https://img.shields.io/librariesio/github/zapodot/embedded-ldap-junit.svg)](https://libraries.io/github/zapodot/embedded-ldap-junit) [![GitHub](https://img.shields.io/github/license/zapodot/embedded-ldap-junit)](https://github.com/zapodot/embedded-ldap-junit/blob/master/LICENSE) [![Analytics](https://ga-beacon.appspot.com/UA-40926073-2/embedded-ldap-junit/README.md)](https://github.com/igrigorik/ga-beacon)

A [JUnit Rule](//github.com/junit-team/junit/wiki/Rules) for running an embedded LDAP server in your JUnit test based on the wonderful [UnboundID LDAP SDK](https://www.ldap.com/unboundid-ldap-sdk-for-java). Inspired by the [Embedded Database JUnit Rule](//github.com/zapodot/embedded-db-junit).
A [JUnit 4 Rule](//github.com/junit-team/junit/wiki/Rules) and [JUnit 5 Extension](https://junit.org/junit5/docs/current/user-guide/#extensions) for running an embedded LDAP server in your JUnit test based on the wonderful [UnboundID LDAP SDK](https://www.ldap.com/unboundid-ldap-sdk-for-java). Inspired by the [Embedded Database JUnit Rule](//github.com/zapodot/embedded-db-junit).

## Why?
* you want to test your LDAP integration code without affecting your LDAP server
Expand Down Expand Up @@ -46,7 +46,9 @@ Java 8 or higher is required. It has proven pretty useful for several users and
libraryDependencies += "org.zapodot" % "embedded-ldap-junit" % "0.8.1"
```

### Add to Junit test
### Add to JUnit test

#### JUnit 4
```java
import com.unboundid.ldap.sdk.LDAPInterface;
import javax.naming.Context;
Expand Down Expand Up @@ -96,3 +98,19 @@ public void testContext() throws Exception {
assertNotNull(user);
}
```

#### JUnit 5
For JUnit 5 tests, simply use `EmbeddedLdapExtensionBuilder` instead of `EmbeddedLdapRuleBuilder`:

```java
@RegisterExtension
public EmbeddedLdapExtension embeddedLdapRule = EmbeddedLdapExtensionBuilder
.newInstance()
.usingDomainDsn("dc=example,dc=com")
.importingLdifs("example.ldif")
.build();

...
```

Both JUnit 4 and JUnit 5 builders share the same API, as do the rule and the extension.
8 changes: 7 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
</developers>
<properties>
<junit.version>4.13</junit.version>
<junit5.version>5.7.0</junit5.version>
<unboundid-ldapsdk.version>5.0.1</unboundid-ldapsdk.version>
<slf4j.version>1.7.30</slf4j.version>
<guava.version>29.0-jre</guava.version>
Expand Down Expand Up @@ -52,6 +53,11 @@
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit5.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
Expand Down Expand Up @@ -264,4 +270,4 @@
</properties>
</profile>
</profiles>
</project>
</project>
57 changes: 1 addition & 56 deletions src/main/java/org/zapodot/junit/ldap/EmbeddedLdapRule.java
Original file line number Diff line number Diff line change
@@ -1,65 +1,10 @@
package org.zapodot.junit.ldap;

import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPInterface;
import org.junit.rules.TestRule;

import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;

/**
* A JUnit rule that may be used as either a @Rule or a @ClassRule
*/
public interface EmbeddedLdapRule extends TestRule {

/**
* For tests depending on the UnboundID LDAP SDK. Returns a proxied version of an Unboundid interface that will be
* closed when the test(s) have been invoked
*
* @return a shared LDAPConnection
* @throws LDAPException if a connection can not be opened
*/
LDAPInterface ldapConnection() throws LDAPException;

/**
* For tests depending on the UnboundID LDAP SDK that needs access to an ${link LDAPConnection} object
* rather than the interface. If your code does not close the connection for you it will be closed on teardown
*
* @return a LDAPConnection connected to the embedded LDAP server
* @throws LDAPException if an exception occurred while establishing the connection
*/
LDAPConnection unsharedLdapConnection() throws LDAPException;

/**
* For tests depending on the standard Java JNDI API
*
* @return a shared Context connected to the in-memory LDAP server
* @throws NamingException if context can not be created
*/
Context context() throws NamingException;

/**
* Like {@link #context()}, but returns a DirContext
*
* @return a DirContext connected to the in-memory LDAP server
* @throws NamingException if a LDAP failure happens during DirContext creation
*/
DirContext dirContext() throws NamingException;

/**
* Gives access to the listening port for the currently running embedded LDAP server.
* This will make it easier to use other integration mechanisms
* <p>
* Note: the embedded LDAP server is by default configured to listen only on the loopback address
* (i.e <em>localhost/127.0.0.1</em>) unless another address has been provided to the builder when the rule was built
* </p>
*
* @return the port number that the embedded server is listening to
* @see org.zapodot.junit.ldap.EmbeddedLdapRuleBuilder#bindingToAddress(String)
*/
int embeddedServerPort();

public interface EmbeddedLdapRule extends EmbeddedLdapServer, TestRule {

}
233 changes: 4 additions & 229 deletions src/main/java/org/zapodot/junit/ldap/EmbeddedLdapRuleBuilder.java
Original file line number Diff line number Diff line change
@@ -1,60 +1,14 @@
package org.zapodot.junit.ldap;

import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.Lists;
import com.google.common.io.Resources;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.schema.Schema;
import com.unboundid.ldif.LDIFException;
import org.zapodot.junit.ldap.internal.AuthenticationConfiguration;
import org.zapodot.junit.ldap.internal.AbstractEmbeddedLdapBuilder;
import org.zapodot.junit.ldap.internal.EmbeddedLdapRuleImpl;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Objects;

/**
* A builder providing a fluent way of defining EmbeddedLdapRule instances
*/
public class EmbeddedLdapRuleBuilder {

public static final String DEFAULT_DOMAIN = "dc=example,dc=com";
public static final String DEFAULT_BIND_DSN = "cn=Directory manager";
public static final String DEFAULT_BIND_CREDENTIALS = "password";
public static final String LDAP_SERVER_LISTENER_NAME = "test-listener";
public static final int MIN_PORT_EXCLUSIVE = 0;
public static final int MAX_PORT_EXCLUSIVE = 65535;
private List<String> domainDsn = new LinkedList<>();

private String bindDSN = DEFAULT_BIND_DSN;

private String bindCredentials = DEFAULT_BIND_CREDENTIALS;

private List<String> ldifsToImport = new LinkedList<>();

private List<String> schemaLdifs = new LinkedList<>();

private boolean addDefaultSchema = true;

private Integer bindPort = 0;

private InetAddress bindAddress = InetAddress.getLoopbackAddress();

private AuthenticationConfiguration authenticationConfiguration;

private InMemoryListenerConfig listenerConfig = null;

public EmbeddedLdapRuleBuilder() {
}
public class EmbeddedLdapRuleBuilder extends AbstractEmbeddedLdapBuilder<EmbeddedLdapRuleBuilder> {

/**
* Creates a new builder
Expand All @@ -65,109 +19,8 @@ public static EmbeddedLdapRuleBuilder newInstance() {
return new EmbeddedLdapRuleBuilder();
}

/**
* Sets a domainDsn to be used. May be multiple values. If not set, it will default to the value of the {@link #DEFAULT_DOMAIN DEFAULT_DOMAIN} field
*
* @param domainDsn a valid DSN string
* @return same EmbeddedLdapRuleBuilder instance with the domainDsn field set
*/
public EmbeddedLdapRuleBuilder usingDomainDsn(final String domainDsn) {
this.domainDsn.add(domainDsn);
return this;
}

/**
* Sets the DSN to bind to when authenticating. If not set, it will default to the value of the {@link #DEFAULT_BIND_DSN DEFAULT_BIND_DSN} field
*
* @param bindDSN a valid DSN string
* @return same EmbeddedLdapRuleBuilder instance with the bindDSN field set
*/
public EmbeddedLdapRuleBuilder usingBindDSN(final String bindDSN) {
this.bindDSN = bindDSN;
return this;
}

/**
* Sets the credentials to be used to authenticate. If not set, it will default to the value of the {@link #DEFAULT_BIND_CREDENTIALS DEFAULT_BIND_CREDENTIALS} field
*
* @param bindCredentials a password string
* @return same EmbeddedLdapRuleBuilder instance with the bindCredentials field set
*/
public EmbeddedLdapRuleBuilder usingBindCredentials(final String bindCredentials) {
this.bindCredentials = bindCredentials;
return this;
}

/**
* Sets the port that the in-memory LDAP server will bind to. If not set, an available port will be picked automatically
*
* @param port a port number
* @return same EmbeddedLdapRuleBuilder instance with the port field set
* @throws IllegalArgumentException if the provided value for port is not between @{link MIN_PORT_EXCLUSIVE}
* and @{MAX_PORT_EXCLUSIVE} (exclusive)
*/
public EmbeddedLdapRuleBuilder bindingToPort(final int port) {
if ((port < MIN_PORT_EXCLUSIVE) || (port > MAX_PORT_EXCLUSIVE)) {
throw new IllegalArgumentException(String.format("Value \"%s\" is not a valid port number", port));
}
this.bindPort = Integer.valueOf(port);
return this;
}

/**
* Allows the listening address for the embedded LDAP server to be set. If not set it will bind to <em>localhost/127.0.0.1</em>.
*
* @param address a valid hostname or textual representation of an IP address
* @return same EmbeddedLdapRuleBuilder instance with the bindAddress field set
* @throws IllegalArgumentException if the value provided for \"address\" is invalid
*/
public EmbeddedLdapRuleBuilder bindingToAddress(final String address) {
Objects.requireNonNull(address);
try {
final InetAddress addressByName = InetAddress.getByName(address);
this.bindAddress = addressByName;
} catch (UnknownHostException e) {
throw new IllegalArgumentException(String.format("Unknown host address \"%s\"", address), e);
}
return this;
}

/**
* Avoid adding UnboundID's default schema that contains the most common LDAP elements defined through various RFC's.
*
* @return same EmbeddedLdapRuleBuilder instance with the withoutDefaultSchema field set to FALSE
*/
public EmbeddedLdapRuleBuilder withoutDefaultSchema() {
this.addDefaultSchema = false;
return this;
}

/**
* Define schemas to be used for the server. If not defined, UnboundID will set up a default schema.
*
* @param ldifSchemaFiles LDIF-files containing schema element definitions
* @return same EmbeddedLdapRuleBuilder with the given LDIF-files added to the internal schema file collection.
*/
public EmbeddedLdapRuleBuilder withSchema(final String... ldifSchemaFiles) {
this.schemaLdifs.addAll(Arrays.asList(ldifSchemaFiles));
return this;
}

/**
* Specify one or more LDIF resources to be imported on startup.
*
* @param ldifFiles LDIF-files to import
* @return same EmbeddedLdapRuleBuilder instance with the provided ldifFiles added to the list of LDIF files to import
*/
public EmbeddedLdapRuleBuilder importingLdifs(final String... ldifFiles) {
if (ldifFiles != null) {
ldifsToImport.addAll(Arrays.asList(ldifFiles));
}
return this;
}

public EmbeddedLdapRuleBuilder withListener(InMemoryListenerConfig listenerConfig) {
this.listenerConfig = listenerConfig;
@Override
protected EmbeddedLdapRuleBuilder getThis() {
return this;
}

Expand All @@ -183,82 +36,4 @@ public EmbeddedLdapRule build() {
ldifsToImport);
}

private InMemoryDirectoryServerConfig createInMemoryServerConfiguration() {
try {
final InMemoryDirectoryServerConfig inMemoryDirectoryServerConfig =
new InMemoryDirectoryServerConfig(domainDsnArray());

if (bindCredentials != null) {
this.authenticationConfiguration = new AuthenticationConfiguration(bindDSN, bindCredentials);
inMemoryDirectoryServerConfig.addAdditionalBindCredentials(bindDSN, bindCredentials);
}

if (listenerConfig == null) {
listenerConfig = InMemoryListenerConfig.createLDAPConfig(
LDAP_SERVER_LISTENER_NAME,
bindAddress,
bindPort,
null);
}
inMemoryDirectoryServerConfig.setListenerConfigs(listenerConfig);
inMemoryDirectoryServerConfig.setSchema(customSchema());
return inMemoryDirectoryServerConfig;
} catch (LDAPException e) {
throw new IllegalStateException(
"Could not create configuration for the in-memory LDAP instance due to an exception",
e);
}
}

private String[] domainDsnArray() {
if (domainDsn.size() == 0) {
return new String[]{DEFAULT_DOMAIN};
} else {
return domainDsn.toArray(new String[]{});
}
}

private Schema customSchema() {
final List<File> schemaFiles = schemaFiles();

try {
final Schema initialSchema = (addDefaultSchema ? Schema.getDefaultStandardSchema() : null);
if (!schemaFiles.isEmpty()) {
final Schema customSchema = initialSchema == null
? Schema.getSchema(schemaFiles)
: Schema.mergeSchemas(initialSchema, Schema.getSchema(schemaFiles));
return customSchema;
} else {
return null;
}

} catch (IOException | LDIFException | LDAPException e) {
throw new IllegalArgumentException(
"Could not create custom LDAP schema due, probably caused by an incorrectly formatted schema",
e);
}
}

private List<File> schemaFiles() {
return Lists.newArrayList(Lists.transform(this.schemaLdifs, new Function<String, File>() {
@Override
public File apply(final String input) {
try {
final File file = new File(Resources.getResource(input).toURI());
if (!file.isFile()) {
throw new IllegalArgumentException(String.format(
"The resource named \"%s\" can not be found or is not a file",
input));
}
return file;
} catch (URISyntaxException e) {
throw new IllegalArgumentException(String.format(
"The resource named \"%s\" is not a valid file reference",
input), e);
}
}
}));
}


}
Loading

0 comments on commit 83625a6

Please sign in to comment.