diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/LdifReaderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/LdifReaderTests.java new file mode 100644 index 0000000000..f5f4f267de --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/LdifReaderTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2026 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.batch.infrastructure.item.ldif; + +import org.junit.jupiter.api.Test; + +import org.springframework.batch.infrastructure.item.ExecutionContext; +import org.springframework.batch.infrastructure.item.ldif.support.LdifReaderTestSupport; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * @author Banseok Kim + */ +class LdifReaderTests extends LdifReaderTestSupport { + + @Test + void readRecordsUsingSetters() throws Exception { + LdifReader reader = new LdifReader(ldifResource); + reader.setName("ldif"); + reader.setStrict(true); + reader.afterPropertiesSet(); + + verify(reader, expectedDns()); + } + + @Test + void missingResourceInStrictModeShouldFailOnOpen() throws Exception { + LdifReader reader = new LdifReader(missingResource()); + reader.setName("ldif"); + reader.setStrict(true); + reader.afterPropertiesSet(); + + assertThrows(Exception.class, () -> reader.open(new ExecutionContext())); + } + + @Test + void skippedRecordsCallbackIsInvoked() throws Exception { + StringBuilder callback = new StringBuilder(); + + LdifReader reader = new LdifReader(ldifResource); + reader.setName("ldif"); + reader.setRecordsToSkip(1); + reader.setSkippedRecordsCallback(attributes -> callback.append(attributes.getName().toString())); + reader.afterPropertiesSet(); + + reader.open(new ExecutionContext()); + reader.read(); + reader.close(); + + assertEquals("cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com", callback.toString()); + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/MappingLdifReaderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/MappingLdifReaderTests.java new file mode 100644 index 0000000000..8013cdcd76 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/MappingLdifReaderTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2026 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.batch.infrastructure.item.ldif; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Test; + +import org.springframework.batch.infrastructure.item.ExecutionContext; +import org.springframework.batch.infrastructure.item.ldif.support.MappingLdifReaderTestSupport; +import org.springframework.ldap.core.LdapAttributes; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * @author Banseok Kim + */ +class MappingLdifReaderTests extends MappingLdifReaderTestSupport { + + @Test + void readRecordsMappingToDnStringUsingSetters() throws Exception { + MappingLdifReader reader = new MappingLdifReader<>(ldifResource); + reader.setName("ldif"); + reader.setRecordMapper(new StringMapper()); + reader.setStrict(true); + reader.afterPropertiesSet(); + + verify(reader, expectedDns()); + } + + @Test + void missingResourceInStrictModeShouldFailOnOpen() throws Exception { + MappingLdifReader reader = new MappingLdifReader<>(missingResource()); + reader.setName("ldif"); + reader.setRecordMapper(new StringMapper()); + reader.setStrict(true); + reader.afterPropertiesSet(); + + assertThrows(Exception.class, () -> reader.open(new ExecutionContext())); + } + + private static class StringMapper implements RecordMapper { + + @Override + public @Nullable String mapRecord(@Nullable LdapAttributes attributes) { + if (attributes == null) { + return null; + } + return attributes.getName().toString(); + } + + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/builder/LdifReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/builder/LdifReaderBuilderTests.java new file mode 100644 index 0000000000..0e18805b2f --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/builder/LdifReaderBuilderTests.java @@ -0,0 +1,158 @@ +/* + * Copyright 2026 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.batch.infrastructure.item.ldif.builder; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.batch.infrastructure.item.ExecutionContext; +import org.springframework.batch.infrastructure.item.ItemStreamException; +import org.springframework.batch.infrastructure.item.ldif.LdifReader; +import org.springframework.batch.infrastructure.item.ldif.RecordCallbackHandler; +import org.springframework.batch.infrastructure.item.ldif.support.LdifReaderTestSupport; +import org.springframework.ldap.core.LdapAttributes; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * @author Banseok Kim + */ +class LdifReaderBuilderTests extends LdifReaderTestSupport { + + private String callbackDn; + + @Test + void itemReaderBasicRead() throws Exception { + LdifReader reader = new LdifReaderBuilder().name("ldif").resource(ldifResource).build(); + + verify(reader, expectedDns()); + } + + @Test + void recordsToSkipSkipsFirstRecord() throws Exception { + LdifReader reader = new LdifReaderBuilder().name("ldif").resource(ldifResource).recordsToSkip(1).build(); + + reader.open(new ExecutionContext()); + LdapAttributes item = reader.read(); + assertEquals("cn=Bjorn Jensen,ou=Accounting,dc=airius,dc=com", item.getName().toString()); + reader.close(); + } + + @Test + void currentItemCountStartsFromThirdRecord() throws Exception { + LdifReader reader = new LdifReaderBuilder().name("ldif").resource(ldifResource).currentItemCount(2).build(); + + reader.open(new ExecutionContext()); + LdapAttributes item = reader.read(); + assertEquals("cn=Gern Jensen,ou=Product Testing,dc=airius,dc=com", item.getName().toString()); + reader.close(); + } + + @Test + void currentItemCountAtEndReturnsNull() throws Exception { + LdifReader reader = new LdifReaderBuilder().name("ldif").resource(ldifResource).currentItemCount(3).build(); + + reader.open(new ExecutionContext()); + Assertions.assertNull(reader.read()); + reader.close(); + } + + @Test + void maxItemCountLimitsReads() throws Exception { + LdifReader reader = new LdifReaderBuilder().name("ldif").resource(ldifResource).maxItemCount(1).build(); + + reader.open(new ExecutionContext()); + LdapAttributes first = reader.read(); + LdapAttributes second = reader.read(); + assertEquals("cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com", first.getName().toString()); + assertNull(second); + reader.close(); + } + + @Test + void skippedRecordsCallbackIsInvoked() throws Exception { + this.callbackDn = null; + + LdifReader reader = new LdifReaderBuilder().name("ldif") + .resource(ldifResource) + .recordsToSkip(1) + .skippedRecordsCallback(new TestCallback()) + .build(); + + reader.open(new ExecutionContext()); + reader.read(); + assertEquals("cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com", this.callbackDn); + reader.close(); + } + + @Test + void saveStateUpdatesExecutionContext() throws Exception { + LdifReader reader = new LdifReaderBuilder().name("foo").resource(ldifResource).build(); + + ExecutionContext ec = new ExecutionContext(); + reader.open(ec); + reader.read(); + reader.update(ec); + + assertEquals(1, ec.getInt("foo.read.count")); + reader.close(); + } + + @Test + void strictTrueThrowsOnMissingResource() throws Exception { + LdifReader reader = new LdifReaderBuilder().name("ldif").resource(missingResource()).strict(true).build(); + + ItemStreamException ex = assertThrows(ItemStreamException.class, () -> reader.open(new ExecutionContext())); + assertEquals("Failed to initialize the reader", ex.getMessage()); + } + + @Test + void strictFalseDoesNotThrowOnMissingResource() throws Exception { + LdifReader reader = new LdifReaderBuilder().name("ldif").resource(missingResource()).strict(false).build(); + + reader.open(new ExecutionContext()); + reader.close(); + } + + @Test + void itemReaderWithNoResourceShouldFail() { + assertThrows(IllegalArgumentException.class, () -> new LdifReaderBuilder().name("ldif").build()); + } + + @Test + void itemReaderWithNoNameAndDefaultSaveStateShouldFail() { + assertThrows(IllegalArgumentException.class, () -> new LdifReaderBuilder().resource(ldifResource).build()); + } + + @Test + void itemReaderWithNoNameButSaveStateFalseShouldSucceed() throws Exception { + LdifReader reader = new LdifReaderBuilder().resource(ldifResource).saveState(false).build(); + + verify(reader, expectedDns()); + } + + private class TestCallback implements RecordCallbackHandler { + + @Override + public void handleRecord(LdapAttributes attributes) { + callbackDn = attributes.getName().toString(); + } + + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/builder/MappingLdifReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/builder/MappingLdifReaderBuilderTests.java new file mode 100644 index 0000000000..5146fa484e --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/builder/MappingLdifReaderBuilderTests.java @@ -0,0 +1,199 @@ +/* + * Copyright 2026 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.batch.infrastructure.item.ldif.builder; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.batch.infrastructure.item.ExecutionContext; +import org.springframework.batch.infrastructure.item.ItemStreamException; +import org.springframework.batch.infrastructure.item.ldif.MappingLdifReader; +import org.springframework.batch.infrastructure.item.ldif.RecordCallbackHandler; +import org.springframework.batch.infrastructure.item.ldif.RecordMapper; +import org.springframework.batch.infrastructure.item.ldif.support.MappingLdifReaderTestSupport; +import org.springframework.ldap.core.LdapAttributes; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * @author Banseok Kim + */ +class MappingLdifReaderBuilderTests extends MappingLdifReaderTestSupport { + + private String callbackDn; + + @Test + void itemReader_basicRead_mapsToDnString() throws Exception { + MappingLdifReader reader = new MappingLdifReaderBuilder().name("ldif") + .resource(ldifResource) + .recordMapper(new StringMapper()) + .build(); + + verify(reader, expectedDns()); + } + + @Test + void recordsToSkip_skipsFirstRecord() throws Exception { + MappingLdifReader reader = new MappingLdifReaderBuilder().name("ldif") + .resource(ldifResource) + .recordsToSkip(1) + .recordMapper(new StringMapper()) + .build(); + + reader.open(new ExecutionContext()); + String item = reader.read(); + assertEquals("cn=Bjorn Jensen,ou=Accounting,dc=airius,dc=com", item); + reader.close(); + } + + @Test + void currentItemCount_startsFromThirdRecord() throws Exception { + MappingLdifReader reader = new MappingLdifReaderBuilder().name("ldif") + .resource(ldifResource) + .currentItemCount(2) + .recordMapper(new StringMapper()) + .build(); + + reader.open(new ExecutionContext()); + String item = reader.read(); + assertEquals("cn=Gern Jensen,ou=Product Testing,dc=airius,dc=com", item); + reader.close(); + } + + @Test + void currentItemCountAtEndReturnsNull() throws Exception { + MappingLdifReader reader = new MappingLdifReaderBuilder().name("ldif") + .resource(ldifResource) + .currentItemCount(3) + .recordMapper(new StringMapper()) + .build(); + + reader.open(new ExecutionContext()); + Assertions.assertNull(reader.read()); + reader.close(); + } + + @Test + void skippedRecordsCallback_isInvoked() throws Exception { + this.callbackDn = null; + + MappingLdifReader reader = new MappingLdifReaderBuilder().name("ldif") + .resource(ldifResource) + .recordsToSkip(1) + .recordMapper(new StringMapper()) + .skippedRecordsCallback(new TestCallback()) + .build(); + + reader.open(new ExecutionContext()); + reader.read(); + assertEquals("cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com", this.callbackDn); + reader.close(); + } + + @Test + void saveState_updatesExecutionContext() throws Exception { + MappingLdifReader reader = new MappingLdifReaderBuilder().name("foo") + .resource(ldifResource) + .recordMapper(new StringMapper()) + .build(); + + ExecutionContext ec = new ExecutionContext(); + reader.open(ec); + reader.read(); + reader.update(ec); + + assertEquals(1, ec.getInt("foo.read.count")); + reader.close(); + } + + @Test + void strictTrue_throwsOnMissingResource() throws Exception { + MappingLdifReader reader = new MappingLdifReaderBuilder().name("ldif") + .resource(missingResource()) + .recordMapper(new StringMapper()) + .strict(true) + .build(); + + ItemStreamException ex = assertThrows(ItemStreamException.class, () -> reader.open(new ExecutionContext())); + assertEquals("Failed to initialize the reader", ex.getMessage()); + } + + @Test + void strictFalse_doesNotThrowOnMissingResource() throws Exception { + MappingLdifReader reader = new MappingLdifReaderBuilder().name("ldif") + .resource(missingResource()) + .recordMapper(new StringMapper()) + .strict(false) + .build(); + + reader.open(new ExecutionContext()); + reader.close(); + } + + @Test + void itemReaderWithNoRecordMapperShouldFail() { + assertThrows(IllegalArgumentException.class, + () -> new MappingLdifReaderBuilder().name("ldif").resource(ldifResource).build()); + } + + @Test + void itemReaderWithNoResourceShouldFail() { + assertThrows(IllegalArgumentException.class, + () -> new MappingLdifReaderBuilder().name("ldif").recordMapper(new StringMapper()).build()); + } + + @Test + void itemReaderWithNoNameAndDefaultSaveStateShouldFail() { + assertThrows(IllegalArgumentException.class, + () -> new MappingLdifReaderBuilder().resource(ldifResource) + .recordMapper(new StringMapper()) + .build()); + } + + @Test + void itemReaderWithNoNameButSaveStateFalseShouldSucceed() throws Exception { + MappingLdifReader reader = new MappingLdifReaderBuilder().resource(ldifResource) + .saveState(false) + .recordMapper(new StringMapper()) + .build(); + + verify(reader, expectedDns()); + } + + private static class StringMapper implements RecordMapper { + + @Override + public @Nullable String mapRecord(@Nullable LdapAttributes attributes) { + if (attributes == null) { + return null; + } + return attributes.getName().toString(); + } + + } + + private class TestCallback implements RecordCallbackHandler { + + @Override + public void handleRecord(LdapAttributes attributes) { + callbackDn = attributes.getName().toString(); + } + + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/support/LdifReaderTestSupport.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/support/LdifReaderTestSupport.java new file mode 100644 index 0000000000..18d91a18f5 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/support/LdifReaderTestSupport.java @@ -0,0 +1,46 @@ +/* + * Copyright 2026 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.batch.infrastructure.item.ldif.support; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.batch.infrastructure.item.ExecutionContext; +import org.springframework.batch.infrastructure.item.ldif.LdifReader; +import org.springframework.ldap.core.LdapAttributes; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Banseok Kim + */ +public abstract class LdifReaderTestSupport extends LdifTestFixtures { + + protected void verify(LdifReader reader, List expectedDns) throws Exception { + reader.open(new ExecutionContext()); + + List actualDns = new ArrayList<>(); + LdapAttributes item; + while ((item = reader.read()) != null) { + actualDns.add(item.getName().toString()); + } + + assertThat(actualDns).containsExactlyElementsOf(expectedDns); + + reader.close(); + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/support/LdifTestFixtures.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/support/LdifTestFixtures.java new file mode 100644 index 0000000000..ec8e3ae60d --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/support/LdifTestFixtures.java @@ -0,0 +1,69 @@ +/* + * Copyright 2026 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.batch.infrastructure.item.ldif.support; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.springframework.core.io.ByteArrayResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; + +/** + * @author Banseok Kim + */ +public abstract class LdifTestFixtures { + + protected static final String LDIF = """ + dn: cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com + objectclass: top + objectclass: person + objectclass: organizationalPerson + cn: Barbara Jensen + sn: Jensen + + dn: cn=Bjorn Jensen,ou=Accounting,dc=airius,dc=com + objectclass: top + objectclass: person + objectclass: organizationalPerson + cn: Bjorn Jensen + sn: Jensen + + dn: cn=Gern Jensen,ou=Product Testing,dc=airius,dc=com + objectclass: top + objectclass: person + objectclass: organizationalPerson + cn: Gern Jensen + sn: Jensen + """; + + protected final Resource ldifResource = new ByteArrayResource(LDIF.getBytes(StandardCharsets.UTF_8), "test.ldif"); + + protected List expectedDns() { + return List.of("cn=Barbara Jensen,ou=Product Development,dc=airius,dc=com", + "cn=Bjorn Jensen,ou=Accounting,dc=airius,dc=com", "cn=Gern Jensen,ou=Product Testing,dc=airius,dc=com"); + } + + protected Resource missingResource() throws IOException { + Path missing = Files.createTempFile("missing-", ".ldif"); + Files.delete(missing); + return new FileSystemResource(missing.toFile()); + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/support/MappingLdifReaderTestSupport.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/support/MappingLdifReaderTestSupport.java new file mode 100644 index 0000000000..d1294afbe3 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/infrastructure/item/ldif/support/MappingLdifReaderTestSupport.java @@ -0,0 +1,48 @@ +/* + * Copyright 2026 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.batch.infrastructure.item.ldif.support; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.batch.infrastructure.item.ExecutionContext; +import org.springframework.batch.infrastructure.item.ldif.MappingLdifReader; +import org.springframework.ldap.core.LdapAttributes; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Banseok Kim + */ +public abstract class MappingLdifReaderTestSupport extends LdifTestFixtures { + + protected void verify(MappingLdifReader reader, List expectedDns) throws Exception { + reader.open(new ExecutionContext()); + try { + List actual = new ArrayList<>(); + String item; + while ((item = reader.read()) != null) { + actual.add(item); + } + assertThat(actual).containsExactlyElementsOf(expectedDns); + } + finally { + reader.close(); + } + } + +}