Skip to content

Commit 8144155

Browse files
committed
Merge pull request #44389 from eddumelendez
* pr/44389: Add ServiceConnection support for LLdapContainer (Testcontainers) Add ServiceConnection support for lldap/lldap (Compose) Closes gh-44389
2 parents f715a36 + 261ea64 commit 8144155

File tree

12 files changed

+292
-1
lines changed

12 files changed

+292
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.docker.compose.service.connection.ldap;
18+
19+
import org.springframework.boot.autoconfigure.ldap.LdapConnectionDetails;
20+
import org.springframework.boot.docker.compose.service.connection.test.DockerComposeTest;
21+
import org.springframework.boot.testsupport.container.TestImage;
22+
23+
import static org.assertj.core.api.Assertions.assertThat;
24+
25+
/**
26+
* Integration tests for {@link LLdapDockerComposeConnectionDetailsFactory}.
27+
*
28+
* @author Eddú Meléndez
29+
*/
30+
class LLdapDockerComposeConnectionDetailsFactoryIntegrationTests {
31+
32+
@DockerComposeTest(composeFile = "lldap-compose.yaml", image = TestImage.LLDAP)
33+
void runCreatesConnectionDetails(LdapConnectionDetails connectionDetails) {
34+
assertThat(connectionDetails.getUsername()).isEqualTo("cn=admin,ou=people,dc=springframework,dc=org");
35+
assertThat(connectionDetails.getPassword()).isEqualTo("somepassword");
36+
assertThat(connectionDetails.getBase()).isEqualTo("dc=springframework,dc=org");
37+
assertThat(connectionDetails.getUrls()).hasSize(1);
38+
assertThat(connectionDetails.getUrls()[0]).startsWith("ldap://");
39+
}
40+
41+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
services:
2+
ldap:
3+
image: '{imageName}'
4+
environment:
5+
- 'LLDAP_LDAP_BASE_DN=dc=springframework,dc=org'
6+
- 'LLDAP_LDAP_USER_PASS=somepassword'
7+
ports:
8+
- "3890"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.docker.compose.service.connection.ldap;
18+
19+
import java.util.Map;
20+
21+
import org.springframework.boot.autoconfigure.ldap.LdapConnectionDetails;
22+
import org.springframework.boot.docker.compose.core.RunningService;
23+
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionDetailsFactory;
24+
import org.springframework.boot.docker.compose.service.connection.DockerComposeConnectionSource;
25+
26+
/**
27+
* {@link DockerComposeConnectionDetailsFactory} to create {@link LdapConnectionDetails}
28+
* for an {@code ldap} service.
29+
*
30+
* @author Eddú Meléndez
31+
*/
32+
class LLdapDockerComposeConnectionDetailsFactory extends DockerComposeConnectionDetailsFactory<LdapConnectionDetails> {
33+
34+
protected LLdapDockerComposeConnectionDetailsFactory() {
35+
super("lldap/lldap");
36+
}
37+
38+
@Override
39+
protected LdapConnectionDetails getDockerComposeConnectionDetails(DockerComposeConnectionSource source) {
40+
return new LLdapDockerComposeConnectionDetails(source.getRunningService());
41+
}
42+
43+
/**
44+
* {@link LdapConnectionDetails} backed by an {@code lldap} {@link RunningService}.
45+
*/
46+
static class LLdapDockerComposeConnectionDetails extends DockerComposeConnectionDetails
47+
implements LdapConnectionDetails {
48+
49+
private final String[] urls;
50+
51+
private final String base;
52+
53+
private final String username;
54+
55+
private final String password;
56+
57+
LLdapDockerComposeConnectionDetails(RunningService service) {
58+
super(service);
59+
Map<String, String> env = service.env();
60+
boolean usesTls = Boolean.parseBoolean(env.getOrDefault("LLDAP_LDAPS_OPTIONS__ENABLED", "false"));
61+
String ldapPort = usesTls ? env.getOrDefault("LLDAP_LDAPS_OPTIONS__PORT", "6360")
62+
: env.getOrDefault("LLDAP_LDAP_PORT", "3890");
63+
this.urls = new String[] { "%s://%s:%d".formatted(usesTls ? "ldaps" : "ldap", service.host(),
64+
service.ports().get(Integer.parseInt(ldapPort))) };
65+
this.base = env.getOrDefault("LLDAP_LDAP_BASE_DN", "dc=example,dc=com");
66+
this.password = env.getOrDefault("LLDAP_LDAP_USER_PASS", "password");
67+
this.username = "cn=admin,ou=people,%s".formatted(this.base);
68+
}
69+
70+
@Override
71+
public String[] getUrls() {
72+
return this.urls;
73+
}
74+
75+
@Override
76+
public String getBase() {
77+
return this.base;
78+
}
79+
80+
@Override
81+
public String getUsername() {
82+
return this.username;
83+
}
84+
85+
@Override
86+
public String getPassword() {
87+
return this.password;
88+
}
89+
90+
}
91+
92+
}

spring-boot-project/spring-boot-docker-compose/src/main/resources/META-INF/spring.factories

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ org.springframework.boot.docker.compose.service.connection.clickhouse.ClickHouse
1414
org.springframework.boot.docker.compose.service.connection.elasticsearch.ElasticsearchDockerComposeConnectionDetailsFactory,\
1515
org.springframework.boot.docker.compose.service.connection.flyway.JdbcAdaptingFlywayConnectionDetailsFactory,\
1616
org.springframework.boot.docker.compose.service.connection.hazelcast.HazelcastDockerComposeConnectionDetailsFactory,\
17+
org.springframework.boot.docker.compose.service.connection.ldap.LLdapDockerComposeConnectionDetailsFactory,\
1718
org.springframework.boot.docker.compose.service.connection.ldap.OpenLdapDockerComposeConnectionDetailsFactory,\
1819
org.springframework.boot.docker.compose.service.connection.liquibase.JdbcAdaptingLiquibaseConnectionDetailsFactory,\
1920
org.springframework.boot.docker.compose.service.connection.mariadb.MariaDbJdbcDockerComposeConnectionDetailsFactory,\

spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/features/dev-services.adoc

+1-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ The following service connections are currently supported:
102102
| 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"
103103

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

107107
| javadoc:org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails[]
108108
| Containers named "mongo" or "bitnami/mongodb"

spring-boot-project/spring-boot-docs/src/docs/antora/modules/reference/pages/testing/testcontainers.adoc

+3
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ The following service connection factories are provided in the `spring-boot-test
6262
| javadoc:org.springframework.boot.autoconfigure.kafka.KafkaConnectionDetails[]
6363
| Containers of type javadoc:org.testcontainers.kafka.KafkaContainer[], javadoc:org.testcontainers.kafka.ConfluentKafkaContainer[] or javadoc:org.testcontainers.redpanda.RedpandaContainer[]
6464

65+
| javadoc:org.springframework.boot.autoconfigure.ldap.LdapConnectionDetails[]
66+
| Containers named "osixia/openldap" or of type javadoc:org.testcontainers.ldap.LLdapContainer[]
67+
6568
| javadoc:org.springframework.boot.autoconfigure.liquibase.LiquibaseConnectionDetails[]
6669
| Containers of type javadoc:{url-testcontainers-jdbc-javadoc}/org.testcontainers.containers.JdbcDatabaseContainer[]
6770

spring-boot-project/spring-boot-testcontainers/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ dependencies {
7070
optional("org.testcontainers:grafana")
7171
optional("org.testcontainers:jdbc")
7272
optional("org.testcontainers:kafka")
73+
optional("org.testcontainers:ldap")
7374
optional("org.testcontainers:mariadb")
7475
optional("org.testcontainers:mongodb")
7576
optional("org.testcontainers:mssqlserver")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.testcontainers.service.connection.ldap;
18+
19+
import java.util.List;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.testcontainers.junit.jupiter.Container;
23+
import org.testcontainers.junit.jupiter.Testcontainers;
24+
import org.testcontainers.ldap.LLdapContainer;
25+
26+
import org.springframework.beans.factory.annotation.Autowired;
27+
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
28+
import org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration;
29+
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
30+
import org.springframework.boot.testsupport.container.TestImage;
31+
import org.springframework.context.annotation.Configuration;
32+
import org.springframework.ldap.core.AttributesMapper;
33+
import org.springframework.ldap.core.LdapTemplate;
34+
import org.springframework.ldap.query.LdapQueryBuilder;
35+
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
36+
37+
import static org.assertj.core.api.Assertions.assertThat;
38+
39+
/**
40+
* Tests for {@link LLdapContainerConnectionDetailsFactory}.
41+
*
42+
* @author Eddú Meléndez
43+
*/
44+
@SpringJUnitConfig
45+
@Testcontainers(disabledWithoutDocker = true)
46+
class LLdapContainerConnectionDetailsFactoryIntegrationTests {
47+
48+
@Container
49+
@ServiceConnection
50+
static final LLdapContainer lldap = TestImage.container(LLdapContainer.class);
51+
52+
@Autowired
53+
private LdapTemplate ldapTemplate;
54+
55+
@Test
56+
void connectionCanBeMadeToLdapContainer() {
57+
List<String> cn = this.ldapTemplate.search(LdapQueryBuilder.query().where("objectClass").is("inetOrgPerson"),
58+
(AttributesMapper<String>) (attributes) -> attributes.get("cn").get().toString());
59+
assertThat(cn).singleElement().isEqualTo("Administrator");
60+
}
61+
62+
@Configuration(proxyBeanMethods = false)
63+
@ImportAutoConfiguration({ LdapAutoConfiguration.class })
64+
static class TestConfiguration {
65+
66+
}
67+
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2012-2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.testcontainers.service.connection.ldap;
18+
19+
import org.testcontainers.ldap.LLdapContainer;
20+
21+
import org.springframework.boot.autoconfigure.ldap.LdapConnectionDetails;
22+
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
23+
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
24+
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
25+
26+
/**
27+
* {@link ContainerConnectionDetailsFactory} to create {@link LdapConnectionDetails} from
28+
* a {@link ServiceConnection @ServiceConnection}-annotated {@link LLdapContainer}.
29+
*
30+
* @author Eddú Meléndez
31+
*/
32+
class LLdapContainerConnectionDetailsFactory
33+
extends ContainerConnectionDetailsFactory<LLdapContainer, LdapConnectionDetails> {
34+
35+
@Override
36+
protected LdapConnectionDetails getContainerConnectionDetails(ContainerConnectionSource<LLdapContainer> source) {
37+
return new LLdapContainerConnectionDetails(source);
38+
}
39+
40+
private static final class LLdapContainerConnectionDetails extends ContainerConnectionDetails<LLdapContainer>
41+
implements LdapConnectionDetails {
42+
43+
private LLdapContainerConnectionDetails(ContainerConnectionSource<LLdapContainer> source) {
44+
super(source);
45+
}
46+
47+
@Override
48+
public String[] getUrls() {
49+
return new String[] { getContainer().getLdapUrl() };
50+
}
51+
52+
@Override
53+
public String getBase() {
54+
return getContainer().getBaseDn();
55+
}
56+
57+
@Override
58+
public String getUsername() {
59+
return getContainer().getUser();
60+
}
61+
62+
@Override
63+
public String getPassword() {
64+
return getContainer().getUserPass();
65+
}
66+
67+
}
68+
69+
}

spring-boot-project/spring-boot-testcontainers/src/main/resources/META-INF/spring.factories

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ org.springframework.boot.testcontainers.service.connection.jdbc.JdbcContainerCon
2222
org.springframework.boot.testcontainers.service.connection.kafka.ApacheKafkaContainerConnectionDetailsFactory,\
2323
org.springframework.boot.testcontainers.service.connection.kafka.ConfluentKafkaContainerConnectionDetailsFactory,\
2424
org.springframework.boot.testcontainers.service.connection.kafka.DeprecatedConfluentKafkaContainerConnectionDetailsFactory,\
25+
org.springframework.boot.testcontainers.service.connection.ldap.LLdapContainerConnectionDetailsFactory,\
2526
org.springframework.boot.testcontainers.service.connection.ldap.OpenLdapContainerConnectionDetailsFactory,\
2627
org.springframework.boot.testcontainers.service.connection.liquibase.LiquibaseContainerConnectionDetailsFactory,\
2728
org.springframework.boot.testcontainers.service.connection.mongo.MongoContainerConnectionDetailsFactory,\

spring-boot-project/spring-boot-tools/spring-boot-test-support-docker/build.gradle

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ dependencies {
2121
optional("org.testcontainers:grafana")
2222
optional("org.testcontainers:junit-jupiter")
2323
optional("org.testcontainers:kafka")
24+
optional("org.testcontainers:ldap")
2425
optional("org.testcontainers:mongodb")
2526
optional("org.testcontainers:neo4j")
2627
optional("org.testcontainers:oracle-xe")

spring-boot-project/spring-boot-tools/spring-boot-test-support-docker/src/main/java/org/springframework/boot/testsupport/container/TestImage.java

+6
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import org.testcontainers.elasticsearch.ElasticsearchContainer;
4040
import org.testcontainers.grafana.LgtmStackContainer;
4141
import org.testcontainers.kafka.ConfluentKafkaContainer;
42+
import org.testcontainers.ldap.LLdapContainer;
4243
import org.testcontainers.redpanda.RedpandaContainer;
4344
import org.testcontainers.utility.DockerImageName;
4445

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

149+
/**
150+
* A container image suitable for testing LLDAP.
151+
*/
152+
LLDAP("lldap/lldap", "v0.6.1-alpine", () -> LLdapContainer.class),
153+
148154
/**
149155
* A container image suitable for testing OpenLDAP.
150156
*/

0 commit comments

Comments
 (0)