Skip to content

Add Service Connection support for lldap/lldap using Compose and Testcontainers #44389

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2012-2025 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.boot.docker.compose.service.connection.ldap;

import org.springframework.boot.autoconfigure.ldap.LdapConnectionDetails;
import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest;
import org.springframework.boot.testsupport.container.TestImage;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Integration tests for {@link LLdapDockerComposeConnectionDetailsFactory}.
*
* @author Eddú Meléndez
*/
class LLdapDockerComposeConnectionDetailsFactoryIntegrationTests {

@DockerComposeTest(composeFile = "lldap-compose.yaml", image = TestImage.LLDAP)
void runCreatesConnectionDetails(LdapConnectionDetails connectionDetails) {
assertThat(connectionDetails.getUsername()).isEqualTo("cn=admin,ou=people,dc=springframework,dc=org");
assertThat(connectionDetails.getPassword()).isEqualTo("somepassword");
assertThat(connectionDetails.getBase()).isEqualTo("dc=springframework,dc=org");
assertThat(connectionDetails.getUrls()).hasSize(1);
assertThat(connectionDetails.getUrls()[0]).startsWith("ldap://");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
ldap:
image: '{imageName}'
environment:
- 'LLDAP_LDAP_BASE_DN=dc=springframework,dc=org'
- 'LLDAP_LDAP_USER_PASS=somepassword'
ports:
- "3890"
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright 2012-2025 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.boot.docker.compose.service.connection.ldap;

import java.util.Map;

import org.springframework.boot.autoconfigure.ldap.LdapConnectionDetails;
import org.springframework.boot.docker.compose.core.RunningService;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;

/**
* {@link DockerComposeConnectionDetailsFactory} to create {@link LdapConnectionDetails}
* for an {@code ldap} service.
*
* @author Eddú Meléndez
*/
class LLdapDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory<LdapConnectionDetails> {

protected LLdapDockerComposeConnectionDetailsFactory() {
super("lldap/lldap");
}

@Override
protected LdapConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
return new LLdapDockerComposeConnectionDetails(source.getRunningService());
}

/**
* {@link LdapConnectionDetails} backed by an {@code lldap} {@link RunningService}.
*/
static class LLdapDockerComposeConnectionDetails extends DockerComposeConnectionDetails
implements LdapConnectionDetails {

private final String[] urls;

private final String base;

private final String username;

private final String password;

LLdapDockerComposeConnectionDetails(RunningService service) {
super(service);
Map<String, String> env = service.env();
boolean usesTls = Boolean.parseBoolean(env.getOrDefault("LLDAP_LDAPS_OPTIONS__ENABLED", "false"));
String ldapPort = usesTls ? env.getOrDefault("LLDAP_LDAPS_OPTIONS__PORT", "6360")
: env.getOrDefault("LLDAP_LDAP_PORT", "3890");
this.urls = new String[] { "%s://%s:%d".formatted(usesTls ? "ldaps" : "ldap", service.host(),
service.ports().get(Integer.parseInt(ldapPort))) };
this.base = env.getOrDefault("LLDAP_LDAP_BASE_DN", "dc=example,dc=com");
this.password = env.getOrDefault("LLDAP_LDAP_USER_PASS", "password");
this.username = "cn=admin,ou=people,%s".formatted(this.base);
}

@Override
public String[] getUrls() {
return this.urls;
}

@Override
public String getBase() {
return this.base;
}

@Override
public String getUsername() {
return this.username;
}

@Override
public String getPassword() {
return this.password;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ org.springframework.boot.docker.compose.service.connection.clickhouse.ClickHouse
org.springframework.boot.docker.compose.service.connection.elasticsearch.ElasticsearchDockerComposeConnectionDetailsFactory,\
org.springframework.boot.docker.compose.service.connection.flyway.JdbcAdaptingFlywayConnectionDetailsFactory,\
org.springframework.boot.docker.compose.service.connection.hazelcast.HazelcastDockerComposeConnectionDetailsFactory,\
org.springframework.boot.docker.compose.service.connection.ldap.LLdapDockerComposeConnectionDetailsFactory,\
org.springframework.boot.docker.compose.service.connection.ldap.OpenLdapDockerComposeConnectionDetailsFactory,\
org.springframework.boot.docker.compose.service.connection.liquibase.JdbcAdaptingLiquibaseConnectionDetailsFactory,\
org.springframework.boot.docker.compose.service.connection.mariadb.MariaDbJdbcDockerComposeConnectionDetailsFactory,\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ The following service connections are currently supported:
| Containers named "clickhouse/clickhouse-server", "bitnami/clickhouse", "gvenzl/oracle-free", "gvenzl/oracle-xe", "mariadb", "bitnami/mariadb", "mssql/server", "mysql", "bitnami/mysql", "postgres", or "bitnami/postgresql"

| javadoc:org.springframework.boot.autoconfigure.ldap.LdapConnectionDetails[]
| Containers named "osixia/openldap"
| Containers named "osixia/openldap", "lldap/lldap"

| javadoc:org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails[]
| Containers named "mongo" or "bitnami/mongodb"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ The following service connection factories are provided in the `spring-boot-test
| javadoc:org.springframework.boot.autoconfigure.kafka.KafkaConnectionDetails[]
| Containers of type javadoc:org.testcontainers.kafka.KafkaContainer[], javadoc:org.testcontainers.kafka.ConfluentKafkaContainer[] or javadoc:org.testcontainers.redpanda.RedpandaContainer[]

| javadoc:org.springframework.boot.autoconfigure.ldap.LdapConnectionDetails[]
| Containers named "osixia/openldap" or of type javadoc:org.testcontainers.ldap.LLdapContainer[]

| javadoc:org.springframework.boot.autoconfigure.liquibase.LiquibaseConnectionDetails[]
| Containers of type javadoc:{url-testcontainers-jdbc-javadoc}/org.testcontainers.containers.JdbcDatabaseContainer[]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ dependencies {
optional("org.testcontainers:grafana")
optional("org.testcontainers:jdbc")
optional("org.testcontainers:kafka")
optional("org.testcontainers:ldap")
optional("org.testcontainers:mariadb")
optional("org.testcontainers:mongodb")
optional("org.testcontainers:mssqlserver")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2012-2025 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.boot.testcontainers.service.connection.ldap;

import java.util.List;

import org.junit.jupiter.api.Test;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.ldap.LLdapContainer;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.query.LdapQueryBuilder;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Tests for {@link LLdapContainerConnectionDetailsFactory}.
*
* @author Eddú Meléndez
*/
@SpringJUnitConfig
@Testcontainers(disabledWithoutDocker = true)
class LLdapContainerConnectionDetailsFactoryIntegrationTests {

@Container
@ServiceConnection
static final LLdapContainer lldap = TestImage.container(LLdapContainer.class);

@Autowired
private LdapTemplate ldapTemplate;

@Test
void connectionCanBeMadeToLdapContainer() {
List<String> cn = this.ldapTemplate.search(LdapQueryBuilder.query().where("objectClass").is("inetOrgPerson"),
(AttributesMapper<String>) (attributes) -> attributes.get("cn").get().toString());
assertThat(cn).singleElement().isEqualTo("Administrator");
}

@Configuration(proxyBeanMethods = false)
@ImportAutoConfiguration({ LdapAutoConfiguration.class })
static class TestConfiguration {

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2012-2025 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.boot.testcontainers.service.connection.ldap;

import org.testcontainers.ldap.LLdapContainer;

import org.springframework.boot.autoconfigure.ldap.LdapConnectionDetails;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;

/**
* {@link ContainerConnectionDetailsFactory} to create {@link LdapConnectionDetails} from
* a {@link ServiceConnection @ServiceConnection}-annotated {@link LLdapContainer}.
*
* @author Eddú Meléndez
*/
class LLdapContainerConnectionDetailsFactory
extends ContainerConnectionDetailsFactory<LLdapContainer, LdapConnectionDetails> {

@Override
protected LdapConnectionDetails getContainerConnectionDetails(ContainerConnectionSource<LLdapContainer> source) {
return new LLdapContainerConnectionDetails(source);
}

private static final class LLdapContainerConnectionDetails extends ContainerConnectionDetails<LLdapContainer>
implements LdapConnectionDetails {

private LLdapContainerConnectionDetails(ContainerConnectionSource<LLdapContainer> source) {
super(source);
}

@Override
public String[] getUrls() {
return new String[] { getContainer().getLdapUrl() };
}

@Override
public String getBase() {
return getContainer().getBaseDn();
}

@Override
public String getUsername() {
return getContainer().getUser();
}

@Override
public String getPassword() {
return getContainer().getUserPass();
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ org.springframework.boot.testcontainers.service.connection.jdbc.JdbcContainerCon
org.springframework.boot.testcontainers.service.connection.kafka.ApacheKafkaContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.kafka.ConfluentKafkaContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.kafka.DeprecatedConfluentKafkaContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.ldap.LLdapContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.ldap.OpenLdapContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.liquibase.LiquibaseContainerConnectionDetailsFactory,\
org.springframework.boot.testcontainers.service.connection.mongo.MongoContainerConnectionDetailsFactory,\
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies {
optional("org.testcontainers:grafana")
optional("org.testcontainers:junit-jupiter")
optional("org.testcontainers:kafka")
optional("org.testcontainers:ldap")
optional("org.testcontainers:mongodb")
optional("org.testcontainers:neo4j")
optional("org.testcontainers:oracle-xe")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.testcontainers.elasticsearch.ElasticsearchContainer;
import org.testcontainers.grafana.LgtmStackContainer;
import org.testcontainers.kafka.ConfluentKafkaContainer;
import org.testcontainers.ldap.LLdapContainer;
import org.testcontainers.redpanda.RedpandaContainer;
import org.testcontainers.utility.DockerImageName;

Expand Down Expand Up @@ -145,6 +146,11 @@ public enum TestImage {
CONFLUENT_KAFKA_DEPRECATED("confluentinc/cp-kafka", "7.4.0",
() -> org.testcontainers.containers.KafkaContainer.class),

/**
* A container image suitable for testing LLDAP.
*/
LLDAP("lldap/lldap", "v0.6.1-alpine", () -> LLdapContainer.class),

/**
* A container image suitable for testing OpenLDAP.
*/
Expand Down