From 87953c123b22eb7f91203a5ea8c0ed3f46b8ac81 Mon Sep 17 00:00:00 2001 From: Matthias Fischer Date: Tue, 23 Jul 2024 11:16:52 +0200 Subject: [PATCH 1/5] feat(impl): [#639] Support any ISO date --- CHANGELOG.md | 1 + .../irs/policystore/common/DateUtils.java | 33 ++++++++- .../irs/policystore/common/DateUtilsTest.java | 72 ++++++++++++++++--- 3 files changed, 94 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2170722db..30b18a733 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ _**For better traceability add the corresponding GitHub issue number in each cha ### Changed +- The date search operators `AFTER_LOCAL_DATE` and `BEFORE_LOCAL_DATE` for fields `createdOn` and `validUntil` support any ISO date time now (relates to #639 and #750). - Improved documentation for `GET /irs/policies/paged` endpoint. #639 ## [5.4.0] - 2024-07-22 diff --git a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java index 6665d74e8..282750ec8 100644 --- a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java +++ b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java @@ -23,6 +23,8 @@ import java.time.LocalTime; import java.time.OffsetDateTime; import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; /** * Date utilities. @@ -34,11 +36,19 @@ private DateUtils() { } public static boolean isDateBefore(final OffsetDateTime dateTime, final String referenceDateString) { - return dateTime.isBefore(toOffsetDateTimeAtStartOfDay(referenceDateString)); + if (isDateWithoutTime(referenceDateString)) { + return dateTime.isBefore(toOffsetDateTimeAtStartOfDay(referenceDateString)); + } else { + return dateTime.isBefore(OffsetDateTime.parse(referenceDateString)); + } } public static boolean isDateAfter(final OffsetDateTime dateTime, final String referenceDateString) { - return dateTime.isAfter(toOffsetDateTimeAtEndOfDay(referenceDateString)); + if (isDateWithoutTime(referenceDateString)) { + return dateTime.isAfter(toOffsetDateTimeAtEndOfDay(referenceDateString)); + } else { + return dateTime.isAfter(OffsetDateTime.parse(referenceDateString)); + } } public static OffsetDateTime toOffsetDateTimeAtStartOfDay(final String dateString) { @@ -48,4 +58,23 @@ public static OffsetDateTime toOffsetDateTimeAtStartOfDay(final String dateStrin public static OffsetDateTime toOffsetDateTimeAtEndOfDay(final String dateString) { return LocalDate.parse(dateString).atTime(LocalTime.MAX).atOffset(ZoneOffset.UTC); } + + public static boolean isDateWithoutTime(final String referenceDateString) { + try { + DateTimeFormatter.ofPattern("yyyy-MM-dd").parse(referenceDateString); + return true; + } catch (DateTimeParseException e) { + try { + OffsetDateTime.parse(referenceDateString, DateTimeFormatter.ISO_DATE); + return true; + } catch (DateTimeParseException e2) { + try { + OffsetDateTime.parse(referenceDateString, DateTimeFormatter.ISO_DATE_TIME); + return false; + } catch (DateTimeParseException e3) { + throw new IllegalArgumentException("Invalid date format: " + referenceDateString); + } + } + } + } } diff --git a/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java b/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java index a1032c03e..cfce3680d 100644 --- a/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java +++ b/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java @@ -20,6 +20,7 @@ package org.eclipse.tractusx.irs.policystore.common; import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; import java.time.LocalDate; import java.time.LocalTime; @@ -27,6 +28,8 @@ import java.time.ZoneOffset; import java.util.stream.Stream; +import org.jetbrains.annotations.NotNull; +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; @@ -40,11 +43,26 @@ void testIsDateBefore(final OffsetDateTime dateTime, final String referenceDateS } static Stream provideDatesForIsDateBefore() { - final OffsetDateTime referenceDateTime = LocalDate.parse("2024-07-05").atStartOfDay().atOffset(ZoneOffset.UTC); return Stream.of( // - Arguments.of(referenceDateTime, "2024-07-04", false), - Arguments.of(referenceDateTime, "2024-07-05", false), - Arguments.of(referenceDateTime, "2024-07-06", true)); + Arguments.of(atStartOfDay("2024-07-05"), "2024-07-04", false), // + Arguments.of(atStartOfDay("2024-07-05"), "2024-07-05", false), // + Arguments.of(atStartOfDay("2024-07-05"), "2024-07-06", true), // + Arguments.of(OffsetDateTime.parse("2024-07-23T15:30:00Z"), "2024-07-23T15:30:00Z", false), // + Arguments.of(OffsetDateTime.parse("2024-07-23T15:30:00Z"), "2024-07-23T15:30:01Z", true), // + Arguments.of(OffsetDateTime.parse("2023-12-01T08:45:00+05:30"), "2023-12-01T08:45:00+05:30", false), // + Arguments.of(OffsetDateTime.parse("2023-12-01T08:45:00+05:30"), "2023-12-01T08:46:01+05:30", true), // + Arguments.of(OffsetDateTime.parse("2022-11-15T22:15:30-04:00"), "2022-11-15T22:15:30-04:00", false), // + Arguments.of(OffsetDateTime.parse("2022-11-15T22:15:30-04:00"), "2022-11-15T22:16:01-04:00", true), // + Arguments.of(OffsetDateTime.parse("2021-06-30T14:00:00.123Z"), "2021-06-30T14:00:00.123Z", false), // + Arguments.of(OffsetDateTime.parse("2021-06-30T14:00:00.123Z"), "2021-06-30T14:00:00.124Z", true), // + Arguments.of(OffsetDateTime.parse("2021-06-29T00:00Z"), "2021-06-29T00:00Z", false), // + Arguments.of(OffsetDateTime.parse("2021-06-29T00:00Z"), "2021-06-30T00:01Z", true) // + ); + } + + @NotNull + private static OffsetDateTime atStartOfDay(final String date) { + return LocalDate.parse(date).atStartOfDay().atOffset(ZoneOffset.UTC); } @ParameterizedTest @@ -54,14 +72,48 @@ void testIsDateAfter(final OffsetDateTime dateTime, final String dateString, fin } static Stream provideDatesForIsDateAfter() { - final OffsetDateTime referenceDateTime = LocalDate.parse("2023-07-05") - .atTime(LocalTime.MAX) - .atOffset(ZoneOffset.UTC); return Stream.of( // - Arguments.of(referenceDateTime, "2023-07-04", true), - Arguments.of(referenceDateTime, "2023-07-05", false), - Arguments.of(referenceDateTime, "2023-07-06", false)); + Arguments.of(atStartOfDay("2024-07-05"), "2024-07-04", true), // + Arguments.of(atStartOfDay("2024-07-05"), "2024-07-05", false), // + Arguments.of(atStartOfDay("2024-07-05"), "2024-07-06", false), // + Arguments.of(OffsetDateTime.parse("2024-07-23T15:30:00Z"), "2024-07-23T15:30:00Z", false), // + Arguments.of(OffsetDateTime.parse("2024-07-23T15:30:00Z"), "2024-07-23T15:29:59Z", true), // + Arguments.of(OffsetDateTime.parse("2023-12-01T08:45:00+05:30"), "2023-12-01T08:45:00+05:30", false), // + Arguments.of(OffsetDateTime.parse("2023-12-01T08:45:00+05:30"), "2023-12-01T08:44:59+05:30", true), // + Arguments.of(OffsetDateTime.parse("2022-11-15T22:15:30-04:00"), "2022-11-15T22:15:30-04:00", false), // + Arguments.of(OffsetDateTime.parse("2022-11-15T22:15:30-04:00"), "2022-11-15T22:15:29-04:00", true), // + Arguments.of(OffsetDateTime.parse("2021-06-30T14:00:00.123Z"), "2021-06-30T14:00:00.123Z", false), // + Arguments.of(OffsetDateTime.parse("2021-06-30T14:00:00.123Z"), "2021-06-30T14:00:00.122Z", true), // + Arguments.of(OffsetDateTime.parse("2021-06-29T00:00Z"), "2021-06-29T00:00Z", false), // + Arguments.of(OffsetDateTime.parse("2021-06-29T00:00Z"), "2021-06-28T00:01Z", true) // + ); + } + + @NotNull + private static OffsetDateTime atEndOfDay(final String dateStr) { + return LocalDate.parse(dateStr).atTime(LocalTime.MAX).atOffset(ZoneOffset.UTC); + } + + @Test + public void testIsDateWithoutTimeWithDateOnly() { + assertThat(DateUtils.isDateWithoutTime("2023-07-23")).isTrue(); + } + + @Test + public void testIsDateWithoutTimeWithDateTime() { + assertThat(DateUtils.isDateWithoutTime("2023-07-23T10:15:30+01:00")).isFalse(); + } + + @Test + public void testIsDateWithoutTimeWithInvalidDate() { + assertThatThrownBy(() -> DateUtils.isDateWithoutTime("invalid-date")).isInstanceOf( + IllegalArgumentException.class).hasMessageContaining("Invalid date format: invalid-date"); + } + + @Test + public void testIsDateWithoutTimeWithDifferentFormatDateTime() { + assertThat(DateUtils.isDateWithoutTime("2023-07-23T10:15:30Z")).isFalse(); } } \ No newline at end of file From 84934fa7e9fe83acaba4bf70a5a7fcde4fe0554b Mon Sep 17 00:00:00 2001 From: Matthias Fischer Date: Tue, 23 Jul 2024 11:38:33 +0200 Subject: [PATCH 2/5] feat(impl): [#639] update documentation concerning ISO date support --- docs/src/api/irs-api.yaml | 13 ++++++----- .../controllers/PolicyStoreController.java | 22 ++++++++++--------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/docs/src/api/irs-api.yaml b/docs/src/api/irs-api.yaml index 3624b9357..8d338c6a7 100644 --- a/docs/src/api/irs-api.yaml +++ b/docs/src/api/irs-api.yaml @@ -1047,13 +1047,14 @@ paths: ```\n \n### Filtering\n \n`search=,[EQUALS|STARTS_WITH|BEFORE_LOCAL_DATE|AFTER_LOCAL_DATE],`.\n\ \ \nExample: `search=BPN,STARTS_WITH,BPNL12&search=policyId,STARTS_WITH,policy2`.\n\ \ \n| Field | Supported Operations | Value Format\ - \ |\n|--------------|------------------------------------------|----------------------|\n\ + \ |\n|--------------|------------------------------------------|------------------------------------|\n\ | `BPN` | `EQUALS`, `STARTS_WITH` | any string \ - \ |\n| `policyId` | `EQUALS`, `STARTS_WITH` | any\ - \ string |\n| `action` | `EQUALS` \ - \ | `use` or `access` |\n| `createdOn` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE`\ - \ | `yyyy-MM-dd` |\n| `validUntil` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE`\ - \ | `yyyy-MM-dd` |\n\n### Sorting\n`sort=[BPN|policyId|action|createdOn|validUntil],[asc|desc]`.\n\ + \ |\n| `policyId` | `EQUALS`, `STARTS_WITH` \ + \ | any string |\n| `action` | `EQUALS`\ + \ | `use` or `access` |\n\ + | `createdOn` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE` | `yyyy-MM-dd` or\ + \ ISO date with time |\n| `validUntil` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE`\ + \ | `yyyy-MM-dd` or ISO date with time |\n\n### Sorting\n`sort=[BPN|policyId|action|createdOn|validUntil],[asc|desc]`.\n\ \ \nExample: `sort=BPN,asc&sort=policyId,desc`. _(default: `BPN,asc`)_\n\n\ ### Paging\n \nExample: `page=1&size=20` _(default page size: 10, maximal\ \ page size: 1000)_\n" diff --git a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/controllers/PolicyStoreController.java b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/controllers/PolicyStoreController.java index beb077c6a..a62bc75fd 100644 --- a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/controllers/PolicyStoreController.java +++ b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/controllers/PolicyStoreController.java @@ -196,7 +196,8 @@ public Map> getPolicies(// @RequestParam(required = false) // @ValidListOfBusinessPartnerNumbers(allowDefault = true) // @Parameter(description = "List of business partner numbers. " - + "This may also contain the value \"default\" in order to query the default policies.") // + + "This may also contain the value \"default\" in order to query the default policies.") + // final List businessPartnerNumbers // ) { @@ -263,7 +264,7 @@ public List autocomplete( @GetMapping("/policies/paged") @ResponseStatus(HttpStatus.OK) @Operation(summary = "Find registered policies that should be accepted in EDC negotiation " - + "(with filtering, sorting and paging).", // + + "(with filtering, sorting and paging).", // description = """ Fetch a page of policies with options to filter and sort. \s @@ -278,13 +279,13 @@ public List autocomplete( \s Example: `search=BPN,STARTS_WITH,BPNL12&search=policyId,STARTS_WITH,policy2`. \s - | Field | Supported Operations | Value Format | - |--------------|------------------------------------------|----------------------| - | `BPN` | `EQUALS`, `STARTS_WITH` | any string | - | `policyId` | `EQUALS`, `STARTS_WITH` | any string | - | `action` | `EQUALS` | `use` or `access` | - | `createdOn` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE` | `yyyy-MM-dd` | - | `validUntil` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE` | `yyyy-MM-dd` | + | Field | Supported Operations | Value Format | + |--------------|------------------------------------------|------------------------------------| + | `BPN` | `EQUALS`, `STARTS_WITH` | any string | + | `policyId` | `EQUALS`, `STARTS_WITH` | any string | + | `action` | `EQUALS` | `use` or `access` | + | `createdOn` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE` | `yyyy-MM-dd` or ISO date with time | + | `validUntil` | `BEFORE_LOCAL_DATE`, `AFTER_LOCAL_DATE` | `yyyy-MM-dd` or ISO date with time | ### Sorting `sort=[BPN|policyId|action|createdOn|validUntil],[asc|desc]`. @@ -326,7 +327,8 @@ public Page getPoliciesPaged(// @RequestParam(required = false) // @ValidListOfBusinessPartnerNumbers(allowDefault = true) // @Parameter(name = "businessPartnerNumbers", description = "List of business partner numbers. " - + "This may also contain the value \"default\" in order to query the default policies.") // + + "This may also contain the value \"default\" in order to query the default policies.") + // final List businessPartnerNumbers) { if (pageable.getPageSize() > MAX_PAGE_SIZE) { From 01dfbf7f31f84b6e19a2bfc76c75a84b6f1452f8 Mon Sep 17 00:00:00 2001 From: Matthias Fischer Date: Tue, 23 Jul 2024 12:07:34 +0200 Subject: [PATCH 3/5] feat(impl): [#639] improve test using ParameterizedTest --- .../irs/policystore/common/DateUtilsTest.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java b/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java index cfce3680d..bc1e301a1 100644 --- a/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java +++ b/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java @@ -95,14 +95,18 @@ private static OffsetDateTime atEndOfDay(final String dateStr) { return LocalDate.parse(dateStr).atTime(LocalTime.MAX).atOffset(ZoneOffset.UTC); } - @Test - public void testIsDateWithoutTimeWithDateOnly() { - assertThat(DateUtils.isDateWithoutTime("2023-07-23")).isTrue(); + @ParameterizedTest + @MethodSource("provideDatesForIsDateWithoutTime") + public void isDateWithoutTime(final String dateString, final boolean expected) { + assertThat(DateUtils.isDateWithoutTime(dateString)).isEqualTo(expected); } - @Test - public void testIsDateWithoutTimeWithDateTime() { - assertThat(DateUtils.isDateWithoutTime("2023-07-23T10:15:30+01:00")).isFalse(); + static Stream provideDatesForIsDateWithoutTime() { + return Stream.of( // + Arguments.of("2023-07-23", true), // + Arguments.of("2023-07-23T10:15:30+01:00", false), // + Arguments.of("2023-07-23T10:15:30Z", false) // + ); } @Test @@ -111,9 +115,4 @@ public void testIsDateWithoutTimeWithInvalidDate() { IllegalArgumentException.class).hasMessageContaining("Invalid date format: invalid-date"); } - @Test - public void testIsDateWithoutTimeWithDifferentFormatDateTime() { - assertThat(DateUtils.isDateWithoutTime("2023-07-23T10:15:30Z")).isFalse(); - } - } \ No newline at end of file From ebff884e01945fb3b408df086be5d3b94ffcfa9d Mon Sep 17 00:00:00 2001 From: Matthias Fischer Date: Tue, 23 Jul 2024 12:08:17 +0200 Subject: [PATCH 4/5] feat(impl): [#639] PMD warning fixed / ignored (false positive) --- .../irs/policystore/common/DateUtils.java | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java index 282750ec8..59fdc7319 100644 --- a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java +++ b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java @@ -26,11 +26,16 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; +import lombok.extern.slf4j.Slf4j; + /** * Date utilities. */ +@Slf4j public final class DateUtils { + public static final String SIMPLE_DATE_WITHOUT_TIME = "yyyy-MM-dd"; + private DateUtils() { // private constructor (utility class) } @@ -59,22 +64,30 @@ public static OffsetDateTime toOffsetDateTimeAtEndOfDay(final String dateString) return LocalDate.parse(dateString).atTime(LocalTime.MAX).atOffset(ZoneOffset.UTC); } + @SuppressWarnings("PMD.PreserveStackTrace") // this is intended here as we try to parse with different formats public static boolean isDateWithoutTime(final String referenceDateString) { try { - DateTimeFormatter.ofPattern("yyyy-MM-dd").parse(referenceDateString); + DateTimeFormatter.ofPattern(SIMPLE_DATE_WITHOUT_TIME).parse(referenceDateString); return true; } catch (DateTimeParseException e) { - try { - OffsetDateTime.parse(referenceDateString, DateTimeFormatter.ISO_DATE); - return true; - } catch (DateTimeParseException e2) { - try { - OffsetDateTime.parse(referenceDateString, DateTimeFormatter.ISO_DATE_TIME); - return false; - } catch (DateTimeParseException e3) { - throw new IllegalArgumentException("Invalid date format: " + referenceDateString); - } - } + // ignore, trying next format below + log.trace(e.getMessage(), e); + } + + try { + OffsetDateTime.parse(referenceDateString, DateTimeFormatter.ISO_DATE); + return true; + } catch (DateTimeParseException e) { + // ignore, trying next format below + log.trace(e.getMessage(), e); + } + + try { + OffsetDateTime.parse(referenceDateString, DateTimeFormatter.ISO_DATE_TIME); + return false; + } catch (DateTimeParseException e) { + log.trace(e.getMessage(), e); + throw new IllegalArgumentException("Invalid date format: " + referenceDateString); } } } From 01fef00a8288673eb994a1b9cb32f3aac01abaab Mon Sep 17 00:00:00 2001 From: Matthias Fischer Date: Tue, 23 Jul 2024 12:29:17 +0200 Subject: [PATCH 5/5] feat(impl): [#639] fix broken tests after merge --- .../eclipse/tractusx/irs/policystore/common/DateUtils.java | 5 ++++- .../tractusx/irs/policystore/common/DateUtilsTest.java | 4 +--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java index b127d9495..4f9f13b6b 100644 --- a/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java +++ b/irs-policy-store/src/main/java/org/eclipse/tractusx/irs/policystore/common/DateUtils.java @@ -50,6 +50,9 @@ public static boolean isDateBefore(final OffsetDateTime dateTime, final String r } public static boolean isDateAfter(final OffsetDateTime dateTime, final String referenceDateString) { + if (StringUtils.isBlank(referenceDateString)) { + throw new IllegalArgumentException("Invalid date: must not be blank!"); + } if (isDateWithoutTime(referenceDateString)) { return dateTime.isAfter(toOffsetDateTimeAtEndOfDay(referenceDateString)); } else { @@ -99,7 +102,7 @@ public static boolean isDateWithoutTime(final String referenceDateString) { return false; } catch (DateTimeParseException e) { log.trace(e.getMessage(), e); - throw new IllegalArgumentException("Invalid date format: " + referenceDateString); + throw new IllegalArgumentException("Invalid date format: " + referenceDateString, e); } } } diff --git a/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java b/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java index 41aedcee3..2a68d3de4 100644 --- a/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java +++ b/irs-policy-store/src/test/java/org/eclipse/tractusx/irs/policystore/common/DateUtilsTest.java @@ -117,8 +117,7 @@ public void testIsDateWithoutTimeWithInvalidDate() { } @ParameterizedTest - @ValueSource(strings = { "3333-11-11T11:11:11.111Z", - "3333-11-", + @ValueSource(strings = { "3333-11-", "2222", "asdf" }) @@ -126,7 +125,6 @@ void testInvalidDate(final String referenceDateStr) { final ThrowingCallable call = () -> DateUtils.isDateAfter(OffsetDateTime.now(), referenceDateStr); assertThatThrownBy(call).isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("Invalid date") - .hasMessageContaining("refer to the documentation") .hasCauseInstanceOf(DateTimeParseException.class); }