This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 9da7e3ab3d FINERACT-2511: Validate dateFormat parameter to return HTTP
400 instead of 500
9da7e3ab3d is described below
commit 9da7e3ab3dd6857ab4d15394343c76f0ada423e0
Author: Harshitmehra-270709 <[email protected]>
AuthorDate: Sun Mar 8 13:25:06 2026 +0530
FINERACT-2511: Validate dateFormat parameter to return HTTP 400 instead of
500
When an invalid dateFormat (e.g., a date value like '02 February 2026'
instead of a pattern like 'dd MMMM yyyy') is provided during client creation,
DateTimeFormatter.ofPattern() throws an IllegalArgumentException that surfaces
as HTTP 500.
This commit:
- Adds a reusable validDateTimeFormatPattern() method to
DataValidatorBuilder that validates date/time format patterns by attempting
DateTimeFormatter.ofPattern() and catching IllegalArgumentException
- Adds dateFormat validation in ClientDataValidator.validateForCreate() and
validateForUpdate() using the new method
- Adds DataValidatorBuilderDateFormatTest with 12 parameterized unit tests
covering valid patterns, invalid patterns, null, and blank edge cases
---
.../core/data/DataValidatorBuilder.java | 31 +++++++++
.../data/DataValidatorBuilderDateFormatTest.java | 73 ++++++++++++++++++++++
.../portfolio/client/data/ClientDataValidator.java | 12 ++++
3 files changed, 116 insertions(+)
diff --git
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
index b8468ebc69..a05a2168f5 100644
---
a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
+++
b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilder.java
@@ -23,6 +23,7 @@ import com.google.gson.JsonArray;
import java.math.BigDecimal;
import java.text.ParseException;
import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -1091,6 +1092,36 @@ public class DataValidatorBuilder {
return this;
}
+ /**
+ * Validates that the value is a valid {@link DateTimeFormatter} pattern.
+ *
+ * <p>
+ * If the value is a non-blank string, this method attempts to create a
{@link DateTimeFormatter} using
+ * {@link DateTimeFormatter#ofPattern(String)}. If the pattern is invalid,
a validation error is added.
+ * </p>
+ *
+ * @return this {@code DataValidatorBuilder} for method chaining
+ */
+ public DataValidatorBuilder validDateTimeFormatPattern() {
+ if (this.value == null && this.ignoreNullValue) {
+ return this;
+ }
+
+ if (this.value != null && this.value instanceof String pattern &&
!StringUtils.isBlank(pattern)) {
+ try {
+ DateTimeFormatter.ofPattern(pattern);
+ } catch (final IllegalArgumentException e) {
+ String validationErrorCode = "validation.msg." + this.resource
+ "." + this.parameter + ".invalid.dateFormat.pattern";
+ String defaultEnglishMessage = "The parameter `" +
this.parameter + "` has an invalid date/time pattern: `" + pattern
+ + "`.";
+ final ApiParameterError error =
ApiParameterError.parameterError(validationErrorCode, defaultEnglishMessage,
this.parameter,
+ pattern);
+ this.dataValidationErrors.add(error);
+ }
+ }
+ return this;
+ }
+
/**
* Throws Exception if validation errors.
*
diff --git
a/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilderDateFormatTest.java
b/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilderDateFormatTest.java
new file mode 100644
index 0000000000..9425ac6b88
--- /dev/null
+++
b/fineract-core/src/test/java/org/apache/fineract/infrastructure/core/data/DataValidatorBuilderDateFormatTest.java
@@ -0,0 +1,73 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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
+ *
+ * http://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.apache.fineract.infrastructure.core.data;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+class DataValidatorBuilderDateFormatTest {
+
+ private static final String RESOURCE = "test";
+ private static final String PARAMETER = "dateFormat";
+
+ @ParameterizedTest
+ @ValueSource(strings = { "dd MMMM yyyy", "yyyy-MM-dd", "dd/MM/yyyy",
"MM/dd/yyyy", "dd-MM-yyyy HH:mm:ss" })
+ void validDateTimeFormatPatternShouldAcceptValidPatterns(final String
pattern) {
+ final List<ApiParameterError> errors = new ArrayList<>();
+ new
DataValidatorBuilder(errors).resource(RESOURCE).parameter(PARAMETER).value(pattern).validDateTimeFormatPattern();
+ assertThat(errors).isEmpty();
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = { "02 February 2026", "not-a-pattern", "P!@#$",
"{{invalid}}" })
+ void validDateTimeFormatPatternShouldRejectInvalidPatterns(final String
pattern) {
+ final List<ApiParameterError> errors = new ArrayList<>();
+ new
DataValidatorBuilder(errors).resource(RESOURCE).parameter(PARAMETER).value(pattern).validDateTimeFormatPattern();
+ assertThat(errors).hasSize(1);
+ assertThat(errors.get(0).getParameterName()).isEqualTo(PARAMETER);
+ assertThat(errors.get(0).getDeveloperMessage()).contains("invalid
date/time pattern");
+ }
+
+ @Test
+ void
validDateTimeFormatPatternShouldAcceptNullValueWhenIgnoreNullEnabled() {
+ final List<ApiParameterError> errors = new ArrayList<>();
+ new
DataValidatorBuilder(errors).resource(RESOURCE).parameter(PARAMETER).value(null).ignoreIfNull().validDateTimeFormatPattern();
+ assertThat(errors).isEmpty();
+ }
+
+ @Test
+ void validDateTimeFormatPatternShouldNotFailOnNullValue() {
+ final List<ApiParameterError> errors = new ArrayList<>();
+ // value is null but ignoreIfNull is NOT set — should still not throw
NPE
+ new
DataValidatorBuilder(errors).resource(RESOURCE).parameter(PARAMETER).value(null).validDateTimeFormatPattern();
+ assertThat(errors).isEmpty();
+ }
+
+ @Test
+ void validDateTimeFormatPatternShouldNotFailOnBlankValue() {
+ final List<ApiParameterError> errors = new ArrayList<>();
+ new
DataValidatorBuilder(errors).resource(RESOURCE).parameter(PARAMETER).value("
").validDateTimeFormatPattern();
+ assertThat(errors).isEmpty();
+ }
+}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientDataValidator.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientDataValidator.java
index ac9922704c..e8f0634ef7 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientDataValidator.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientDataValidator.java
@@ -212,6 +212,12 @@ public final class ClientDataValidator {
.integerGreaterThanZero();
}
+ if
(this.fromApiJsonHelper.parameterExists(ClientApiConstants.dateFormatParamName,
element)) {
+ final String dateFormat =
this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.dateFormatParamName,
element);
+
baseDataValidator.reset().parameter(ClientApiConstants.dateFormatParamName).value(dateFormat).notBlank()
+ .validDateTimeFormatPattern();
+ }
+
final Integer legalFormId =
this.fromApiJsonHelper.extractIntegerSansLocaleNamed(ClientApiConstants.legalFormIdParamName,
element);
baseDataValidator.reset().parameter(ClientApiConstants.legalFormIdParamName).value(legalFormId).notNull().inMinMaxRange(1,
2);
@@ -501,6 +507,12 @@ public final class ClientDataValidator {
.validateDateBefore(DateUtils.getBusinessLocalDate()).validateDateBefore(submittedDate);
}
+ if
(this.fromApiJsonHelper.parameterExists(ClientApiConstants.dateFormatParamName,
element)) {
+ final String dateFormat =
this.fromApiJsonHelper.extractStringNamed(ClientApiConstants.dateFormatParamName,
element);
+
baseDataValidator.reset().parameter(ClientApiConstants.dateFormatParamName).value(dateFormat).notBlank()
+ .validDateTimeFormatPattern();
+ }
+
if
(this.fromApiJsonHelper.parameterExists(ClientApiConstants.legalFormIdParamName,
element)) {
atLeastOneParameterPassedForUpdate = true;
final Integer legalFormId =
this.fromApiJsonHelper.extractIntegerSansLocaleNamed(ClientApiConstants.legalFormIdParamName,