Integrating Testcontainers in a Spring Boot LDAP API
Written on
Chapter 1: Overview
This article outlines the steps to configure Testcontainers in a Spring Boot application, enabling the creation of an OpenLDAP Docker container during testing. This setup will help ensure the proper functioning of the LDAP-secured API.
The project we will examine is a straightforward Spring Boot REST API known as spring-boot-ldap-simple-api. For comprehensive code and implementation details, refer to the article linked below. Follow along with the steps provided to get started.
Once you've completed the spring-boot-ldap-simple-api application, we can proceed to implement the testing code.
Section 1.1: Updating the API
To begin, we need to update the pom.xml file. Add the following Testcontainers dependencies by inserting the highlighted sections:
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
...
Section 1.2: Creating the LDIF Test File
In the root directory of your Simple API application, create a file titled test-ldap-mycompany-com.ldif with the following content:
dn: ou=groups,dc=mycompany,dc=com
objectclass: organizationalUnit
objectclass: top
ou: groups
dn: cn=user,ou=groups,dc=mycompany,dc=com
cn: user
gidnumber: 500
objectclass: posixGroup
objectclass: top
dn: ou=users,dc=mycompany,dc=com
objectclass: organizationalUnit
objectclass: top
ou: users
dn: uid=app-user-test,ou=users,dc=mycompany,dc=com
cn: App User Test
gidnumber: 500
givenname: App
homedirectory: /home/users/app-user-test
objectclass: inetOrgPerson
objectclass: posixAccount
objectclass: top
sn: User Test
uid: app-user-test
uidnumber: 1000
userpassword: {MD5}ICy5YqxZB1uWSwcVLSNLcA==
This LDIF (LDAP Directory Interchange Format) file defines the OpenLDAP users with a preset structure for "mycompany.com," including a group called "users" and a user identified as "App User Test," with the username app-user-test and password 123.
Section 1.3: Implementing the SimpleApiControllerTest Class
Next, we will create the SimpleApiControllerTest class in the package com.example.springbootldapsimpleapi located in the src/test/java directory. Populate the class with the following code:
package com.example.springbootldapsimpleapi;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import java.io.IOException;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
@Testcontainers
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class SimpleApiControllerTest {
private static final String PUBLIC_URL = "/public";
private static final String SECURED_URL = "/secured";
private static final String APP_USER_TEST_USERNAME = "app-user-test";
private static final String APP_USER_TEST_PASSWORD = "123";
private static final int OPENLDAP_EXPOSED_PORT = 389;
@Autowired
private TestRestTemplate testRestTemplate;
@Container
private static final GenericContainer openldapContainer = new GenericContainer<>("osixia/openldap:1.5.0")
.withNetworkAliases("openldap")
.withEnv("LDAP_ORGANISATION", "MyCompany Inc.")
.withEnv("LDAP_DOMAIN", "mycompany.com")
.withExposedPorts(OPENLDAP_EXPOSED_PORT)
.withFileSystemBind(
System.getProperty("user.dir") + "/test-ldap-mycompany-com.ldif",
"/ldap/ldap-mycompany-com.ldif",
BindMode.READ_ONLY);
@DynamicPropertySource
static void dynamicProperties(DynamicPropertyRegistry registry) {
String openldapUrl = "ldap://localhost:%s".formatted(openldapContainer.getMappedPort(OPENLDAP_EXPOSED_PORT));
registry.add("spring.ldap.urls", () -> openldapUrl);
}
@BeforeAll
static void beforeAll() throws IOException, InterruptedException {
openldapContainer.execInContainer("ldapadd", "-x", "-D", "cn=admin,dc=mycompany,dc=com", "-w", "admin", "-H", "ldap://", "-f", "ldap/ldap-mycompany-com.ldif");}
@Test
void testPublicEndpoint() {
ResponseEntity<String> responseEntity = testRestTemplate.getForEntity(PUBLIC_URL, String.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isNotNull();
assertThat(responseEntity.getBody()).isEqualTo("Hi World, I am a public endpoint");
}
@Test
void testSecuredEndpointWithoutAuthentication() {
ResponseEntity<String> responseEntity = testRestTemplate.getForEntity(SECURED_URL, String.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
@Test
void testSecuredEndpointWithValidAuthentication() {
ResponseEntity<String> responseEntity = testRestTemplate
.withBasicAuth(APP_USER_TEST_USERNAME, APP_USER_TEST_PASSWORD)
.getForEntity(SECURED_URL, String.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isNotNull();
assertThat(responseEntity.getBody()).isEqualTo("Hi app-user-test, I am a secured endpoint");
}
@ParameterizedTest
@MethodSource("provideInvalidCredentials")
void testSecuredEndpointWithInvalidAuthentication(String username, String password) {
ResponseEntity<String> responseEntity = testRestTemplate
.withBasicAuth(username, password)
.getForEntity(SECURED_URL, String.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
}
private static Stream<Arguments> provideInvalidCredentials() {
return Stream.of(
Arguments.of("", ""),
Arguments.of(" ", " "),
Arguments.of(APP_USER_TEST_USERNAME, "invalid_password"),
Arguments.of("invalid_username", APP_USER_TEST_PASSWORD)
);
}
}
The @Testcontainers annotation indicates that Testcontainers should manage the containers associated with this test class. A GenericContainer is used to initialize an OpenLDAP container, where we set parameters such as LDAP_ORGANISATION and LDAP_DOMAIN to configure OpenLDAP. This container is linked to an LDIF file to populate data. The @Container annotation signals that Testcontainers oversees this container.
In the dynamicProperties method, we specify properties for the Spring application context. We set the spring.ldap.urls property to point to the URL of the OpenLDAP container.
The test cases are straightforward. In the first test, testPublicEndpoint, a request is made to the /public endpoint, expecting a 200 OK status and the response "Hi World, I am a public endpoint".
The subsequent tests focus on the /secured endpoint, checking for a 401 UNAUTHORIZED response when no credentials (in testSecuredEndpointWithoutAuthentication) or invalid credentials (in testSecuredEndpointWithInvalidAuthentication) are provided.
Finally, testSecuredEndpointWithValidAuthentication tests access with valid credentials, expecting a 200 OK status and the response "Hi user.test, I am a secured endpoint".
Section 1.4: Running Test Cases
To execute the tests, navigate to the root folder of the Simple API application in the terminal and run the following command:
./mvnw clean test
Before the tests commence, Testcontainers will start an OpenLDAP Docker container. You can view the logs, which indicate the container's creation and startup.
Once the OpenLDAP container is operational, the SimpleApiControllerTest class will configure OpenLDAP using the LDIF test file located in the project's root directory.
After setting up OpenLDAP, the tests will be executed. If everything is functioning correctly, you should see an output similar to:
[INFO] Results:
[INFO]
[INFO] Tests run: 8, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
Chapter 2: Conclusion
In this article, we explored the seamless integration of Testcontainers within a simple Spring Boot REST API secured by LDAP. Testcontainers facilitated the setup of an OpenLDAP Docker container during testing, providing a secure environment for our API. We thoroughly assessed both public and secured endpoints to confirm the API operates as intended.
Support and Engagement
If you found this article helpful and wish to show your support, consider the following actions:
๐ Engage by clapping, highlighting, and replying to my story. I would be glad to address any questions you may have.
๐ Share my story on social media.
๐ Follow me on: Medium | LinkedIn | Twitter | GitHub.
โ๏ธ Subscribe to my newsletter to stay updated with my latest posts.
Chapter 3: Video Resources
Description: This video titled "Spring Boot Testcontainers - Integration Testing made easy!" provides insights into efficiently using Testcontainers for integration testing in Spring Boot applications.
Description: In this video, "Testcontainers and Spring Boot from integration tests to local development!" Oleg ล elajev discusses how to leverage Testcontainers for both integration tests and local development in Spring Boot projects.