From 97a41802cf20c634e1837ef925d7829a457b54da Mon Sep 17 00:00:00 2001 From: Rain Ramm Date: Tue, 20 Jan 2026 15:13:03 +0200 Subject: [PATCH] Improve test coverage --- .../ControllerAdvisorIntegrationTests.java | 127 +++++++++++++++ .../model/exception/CriteriaResponseTest.java | 137 ++++++++++++++++ .../PersistenceErrorResponseTest.java | 151 ++++++++++++++++++ .../testcomponents/TestPingController.java | 66 +++++++- ...ConfigurationDisabledIntegrationTests.java | 32 ++++ ...pperAutoConfigurationIntegrationTests.java | 108 +++++++++++++ .../ObjectMapperPropertiesTest.java | 46 ++++++ .../TrimmedStringDeserializerTest.java | 93 +++++++++++ 8 files changed, 755 insertions(+), 5 deletions(-) create mode 100644 src/test/java/ee/bitweb/core/api/model/exception/CriteriaResponseTest.java create mode 100644 src/test/java/ee/bitweb/core/api/model/exception/PersistenceErrorResponseTest.java create mode 100644 src/test/java/ee/bitweb/core/object_mapper/ObjectMapperAutoConfigurationDisabledIntegrationTests.java create mode 100644 src/test/java/ee/bitweb/core/object_mapper/ObjectMapperAutoConfigurationIntegrationTests.java create mode 100644 src/test/java/ee/bitweb/core/object_mapper/ObjectMapperPropertiesTest.java create mode 100644 src/test/java/ee/bitweb/core/object_mapper/deserializer/TrimmedStringDeserializerTest.java diff --git a/src/test/java/ee/bitweb/core/api/model/exception/ControllerAdvisorIntegrationTests.java b/src/test/java/ee/bitweb/core/api/model/exception/ControllerAdvisorIntegrationTests.java index 2e97078..71ebc6e 100644 --- a/src/test/java/ee/bitweb/core/api/model/exception/ControllerAdvisorIntegrationTests.java +++ b/src/test/java/ee/bitweb/core/api/model/exception/ControllerAdvisorIntegrationTests.java @@ -427,6 +427,133 @@ void testDateFieldMethodArgumentNotValidException() throws Exception { assertIdField(result); } + @Test + @DisplayName("BindException: Should return validation errors for missing required form fields") + void onBindExceptionWithMissingRequiredFieldsShouldReturnBadRequest() throws Exception { + MockHttpServletRequestBuilder mockMvcBuilder = get(TestPingController.BASE_URL + "/validated-complex") + .header(TRACE_ID_HEADER_NAME, "1234567890"); + + ResultActions result = mockMvc.perform(mockMvcBuilder).andDo(print()); + ResponseAssertions.assertValidationErrorResponse( + result, + List.of( + Error.notNull("age"), + Error.notBlank("email"), + Error.notBlank("name"), + Error.notNull("name") + ) + ); + assertIdField(result); + } + + @Test + @DisplayName("BindException: Should return validation errors for blank fields") + void onBindExceptionWithBlankFieldsShouldReturnBadRequest() throws Exception { + MockHttpServletRequestBuilder mockMvcBuilder = get(TestPingController.BASE_URL + "/validated-complex") + .header(TRACE_ID_HEADER_NAME, "1234567890") + .param("name", "") + .param("age", "25") + .param("email", " "); + + ResultActions result = mockMvc.perform(mockMvcBuilder).andDo(print()); + ResponseAssertions.assertValidationErrorResponse( + result, + List.of( + Error.notBlank("email"), + Error.notBlank("name") + ) + ); + assertIdField(result); + } + + @Test + @DisplayName("BindException: Should return type mismatch error for invalid integer") + void onBindExceptionWithTypeMismatchShouldReturnBadRequest() throws Exception { + MockHttpServletRequestBuilder mockMvcBuilder = get(TestPingController.BASE_URL + "/validated-complex") + .header(TRACE_ID_HEADER_NAME, "1234567890") + .param("name", "John") + .param("age", "not-a-number") + .param("email", "john@example.com"); + + ResultActions result = mockMvc.perform(mockMvcBuilder).andDo(print()); + ResponseAssertions.assertValidationErrorResponse( + result, + List.of( + new Error("age", "typeMismatch", "Unable to interpret value: not-a-number") + ) + ); + assertIdField(result); + } + + @Test + @DisplayName("BindException: Should return multiple errors for type mismatch and validation") + void onBindExceptionWithMixedErrorsShouldReturnBadRequest() throws Exception { + MockHttpServletRequestBuilder mockMvcBuilder = get(TestPingController.BASE_URL + "/validated-complex") + .header(TRACE_ID_HEADER_NAME, "1234567890") + .param("name", "") + .param("age", "invalid") + .param("email", "test@example.com"); + + ResultActions result = mockMvc.perform(mockMvcBuilder).andDo(print()); + ResponseAssertions.assertValidationErrorResponse( + result, + List.of( + new Error("age", "typeMismatch", "Unable to interpret value: invalid"), + Error.notBlank("name") + ) + ); + assertIdField(result); + } + + @Test + @DisplayName("BindException: Should handle type mismatch on form binding without @Valid") + void onPureBindExceptionWithTypeMismatchShouldReturnBadRequest() throws Exception { + MockHttpServletRequestBuilder mockMvcBuilder = get(TestPingController.BASE_URL + "/form-binding") + .header(TRACE_ID_HEADER_NAME, "1234567890") + .param("name", "John") + .param("count", "not-a-number"); + + ResultActions result = mockMvc.perform(mockMvcBuilder).andDo(print()); + ResponseAssertions.assertValidationErrorResponse( + result, + List.of( + new Error("count", "typeMismatch", "Unable to interpret value: not-a-number") + ) + ); + assertIdField(result); + } + + @Test + @DisplayName("BindException: Should handle multiple type mismatches on form binding without @Valid") + void onPureBindExceptionWithMultipleTypeMismatchesShouldReturnBadRequest() throws Exception { + MockHttpServletRequestBuilder mockMvcBuilder = get(TestPingController.BASE_URL + "/form-binding") + .header(TRACE_ID_HEADER_NAME, "1234567890") + .param("count", "abc") + .param("date", "invalid-date"); + + ResultActions result = mockMvc.perform(mockMvcBuilder).andDo(print()); + ResponseAssertions.assertValidationErrorResponse( + result, + List.of( + new Error("count", "typeMismatch", "Unable to interpret value: abc"), + new Error("date", "typeMismatch", "Unable to interpret value: invalid-date") + ) + ); + assertIdField(result); + } + + @Test + @DisplayName("BindException: Should handle explicitly thrown BindException") + void onExplicitBindExceptionShouldReturnBadRequest() throws Exception { + MockHttpServletRequestBuilder mockMvcBuilder = get(TestPingController.BASE_URL + "/throw-bind-exception") + .header(TRACE_ID_HEADER_NAME, "1234567890"); + + ResultActions result = mockMvc.perform(mockMvcBuilder).andDo(print()); + result.andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.id", is("1234567890_generated-trace-id"))) + .andExpect(jsonPath("$.message", is("INVALID_ARGUMENT"))); + } + private void testFieldPost(String field, String value, String expectedReason, String expectedMessage) throws Exception { MockHttpServletRequestBuilder mockMvcBuilder = post(TestPingController.BASE_URL) .header(TRACE_ID_HEADER_NAME, "1234567890") diff --git a/src/test/java/ee/bitweb/core/api/model/exception/CriteriaResponseTest.java b/src/test/java/ee/bitweb/core/api/model/exception/CriteriaResponseTest.java new file mode 100644 index 0000000..e939d4a --- /dev/null +++ b/src/test/java/ee/bitweb/core/api/model/exception/CriteriaResponseTest.java @@ -0,0 +1,137 @@ +package ee.bitweb.core.api.model.exception; + +import ee.bitweb.core.exception.persistence.Criteria; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +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 java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class CriteriaResponseTest { + + @Test + @DisplayName("Should create from Criteria object") + void shouldCreateFromCriteriaObject() { + Criteria criteria = new Criteria("id", "123"); + + CriteriaResponse response = new CriteriaResponse(criteria); + + assertAll( + () -> assertEquals("id", response.getField()), + () -> assertEquals("123", response.getValue()) + ); + } + + @Test + @DisplayName("Should create with direct parameters") + void shouldCreateWithDirectParameters() { + CriteriaResponse response = new CriteriaResponse("email", "test@example.com"); + + assertAll( + () -> assertEquals("email", response.getField()), + () -> assertEquals("test@example.com", response.getValue()) + ); + } + + static Stream comparisonCases() { + return Stream.of( + Arguments.of("alpha", "1", "beta", "1", -1, "field alpha < beta"), + Arguments.of("beta", "1", "alpha", "1", 1, "field beta > alpha"), + Arguments.of("field", "1", "field", "1", 0, "equal fields and values"), + Arguments.of("field", "aaa", "field", "bbb", -1, "same field, value aaa < bbb"), + Arguments.of("field", "bbb", "field", "aaa", 1, "same field, value bbb > aaa") + ); + } + + @ParameterizedTest(name = "compareTo: {5}") + @MethodSource("comparisonCases") + void shouldCompareCorrectly(String field1, String value1, String field2, String value2, + int expectedSign, String description) { + CriteriaResponse first = new CriteriaResponse(field1, value1); + CriteriaResponse second = new CriteriaResponse(field2, value2); + + int result = first.compareTo(second); + + assertEquals(expectedSign, Integer.signum(result)); + } + + @Test + @DisplayName("Should compare case-insensitively") + void shouldCompareCaseInsensitively() { + CriteriaResponse lower = new CriteriaResponse("email", "value"); + CriteriaResponse upper = new CriteriaResponse("EMAIL", "VALUE"); + + assertEquals(0, lower.compareTo(upper)); + } + + @Test + @DisplayName("Should handle null field - nulls come first") + void shouldHandleNullFieldNullsFirst() { + CriteriaResponse withNull = new CriteriaResponse(null, "value"); + CriteriaResponse withValue = new CriteriaResponse("field", "value"); + + assertTrue(withNull.compareTo(withValue) < 0); + assertTrue(withValue.compareTo(withNull) > 0); + } + + @Test + @DisplayName("Should handle null value - nulls come first") + void shouldHandleNullValueNullsFirst() { + CriteriaResponse withNull = new CriteriaResponse("field", null); + CriteriaResponse withValue = new CriteriaResponse("field", "value"); + + assertTrue(withNull.compareTo(withValue) < 0); + assertTrue(withValue.compareTo(withNull) > 0); + } + + @Test + @DisplayName("Should handle both null fields") + void shouldHandleBothNullFields() { + CriteriaResponse first = new CriteriaResponse(null, "a"); + CriteriaResponse second = new CriteriaResponse(null, "b"); + + assertTrue(first.compareTo(second) < 0); + } + + @Test + @DisplayName("Should handle comparison with null object") + void shouldHandleComparisonWithNullObject() { + CriteriaResponse response = new CriteriaResponse("field", "value"); + + assertTrue(response.compareTo(null) > 0); + } + + @Test + @DisplayName("Should be equal when field and value match") + void shouldBeEqualWhenFieldAndValueMatch() { + CriteriaResponse first = new CriteriaResponse("field", "value"); + CriteriaResponse second = new CriteriaResponse("field", "value"); + + assertEquals(first, second); + assertEquals(first.hashCode(), second.hashCode()); + } + + @Test + @DisplayName("Should not be equal when field differs") + void shouldNotBeEqualWhenFieldDiffers() { + CriteriaResponse first = new CriteriaResponse("field1", "value"); + CriteriaResponse second = new CriteriaResponse("field2", "value"); + + assertNotEquals(first, second); + } + + @Test + @DisplayName("Should not be equal when value differs") + void shouldNotBeEqualWhenValueDiffers() { + CriteriaResponse first = new CriteriaResponse("field", "value1"); + CriteriaResponse second = new CriteriaResponse("field", "value2"); + + assertNotEquals(first, second); + } +} diff --git a/src/test/java/ee/bitweb/core/api/model/exception/PersistenceErrorResponseTest.java b/src/test/java/ee/bitweb/core/api/model/exception/PersistenceErrorResponseTest.java new file mode 100644 index 0000000..f7f3d52 --- /dev/null +++ b/src/test/java/ee/bitweb/core/api/model/exception/PersistenceErrorResponseTest.java @@ -0,0 +1,151 @@ +package ee.bitweb.core.api.model.exception; + +import ee.bitweb.core.exception.persistence.Criteria; +import ee.bitweb.core.exception.persistence.EntityNotFoundException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class PersistenceErrorResponseTest { + + @Test + @DisplayName("Should create from PersistenceException") + void shouldCreateFromPersistenceException() { + EntityNotFoundException exception = new EntityNotFoundException( + "User not found", + "User", + "id", + "123" + ); + + PersistenceErrorResponse response = new PersistenceErrorResponse("trace-1", exception); + + assertAll( + () -> assertEquals("trace-1", response.getId()), + () -> assertEquals("User not found", response.getMessage()), + () -> assertEquals("User", response.getEntity()), + () -> assertEquals(1, response.getCriteria().size()) + ); + } + + @Test + @DisplayName("Should create with direct parameters") + void shouldCreateWithDirectParameters() { + Set criteria = Set.of( + new Criteria("id", "123"), + new Criteria("status", "ACTIVE") + ); + + PersistenceErrorResponse response = new PersistenceErrorResponse( + "trace-2", + "Entity not found", + "User", + criteria + ); + + assertAll( + () -> assertEquals("trace-2", response.getId()), + () -> assertEquals("Entity not found", response.getMessage()), + () -> assertEquals("User", response.getEntity()), + () -> assertEquals(2, response.getCriteria().size()) + ); + } + + @Test + @DisplayName("Should sort criteria by field name") + void shouldSortCriteriaByFieldName() { + Set criteria = Set.of( + new Criteria("status", "ACTIVE"), + new Criteria("id", "123"), + new Criteria("email", "test@example.com") + ); + + PersistenceErrorResponse response = new PersistenceErrorResponse( + "trace-3", + "Error", + "User", + criteria + ); + + List list = new ArrayList<>(response.getCriteria()); + + assertAll( + () -> assertEquals("email", list.get(0).getField()), + () -> assertEquals("id", list.get(1).getField()), + () -> assertEquals("status", list.get(2).getField()) + ); + } + + @Test + @DisplayName("Should sort criteria by value when fields are equal") + void shouldSortCriteriaByValueWhenFieldsEqual() { + Set criteria = Set.of( + new Criteria("status", "INACTIVE"), + new Criteria("status", "ACTIVE"), + new Criteria("status", "PENDING") + ); + + PersistenceErrorResponse response = new PersistenceErrorResponse( + "trace-4", + "Error", + "User", + criteria + ); + + List list = new ArrayList<>(response.getCriteria()); + + assertAll( + () -> assertEquals("ACTIVE", list.get(0).getValue()), + () -> assertEquals("INACTIVE", list.get(1).getValue()), + () -> assertEquals("PENDING", list.get(2).getValue()) + ); + } + + @Test + @DisplayName("Should handle null values in criteria") + void shouldHandleNullValuesInCriteria() { + Set criteria = Set.of( + new Criteria("id", "123"), + new Criteria("email", null) + ); + + PersistenceErrorResponse response = new PersistenceErrorResponse( + "trace-5", + "Error", + "User", + criteria + ); + + List list = new ArrayList<>(response.getCriteria()); + + assertAll( + () -> assertEquals("email", list.get(0).getField()), + () -> assertNull(list.get(0).getValue()), + () -> assertEquals("id", list.get(1).getField()) + ); + } + + @Test + @DisplayName("Should remove duplicate criteria") + void shouldRemoveDuplicateCriteria() { + Set criteria = Set.of( + new Criteria("id", "123") + ); + + PersistenceErrorResponse response = new PersistenceErrorResponse( + "trace-6", + "Error", + "User", + criteria + ); + + assertEquals(1, response.getCriteria().size()); + } +} diff --git a/src/test/java/ee/bitweb/core/api/testcomponents/TestPingController.java b/src/test/java/ee/bitweb/core/api/testcomponents/TestPingController.java index b4edd0e..d8970d9 100644 --- a/src/test/java/ee/bitweb/core/api/testcomponents/TestPingController.java +++ b/src/test/java/ee/bitweb/core/api/testcomponents/TestPingController.java @@ -24,6 +24,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.catalina.connector.ClientAbortException; import org.springframework.http.HttpStatus; +import org.springframework.validation.BindException; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -52,19 +53,46 @@ public void postValidated(@Valid @RequestBody ComplexValidatedObject data) { } @GetMapping("/validated") - public void getValidated(@Valid SimpleValidatedObject data) {} + public void getValidated(@Valid SimpleValidatedObject data) { + // nothing to do + } + + @GetMapping("/validated-complex") + public void getValidatedComplex(@Valid ComplexValidatedFormObject data) { + // nothing to do + } + + @GetMapping("/form-binding") + public void formBinding(FormBindingObject data) { + // nothing to do + } + + @GetMapping("/throw-bind-exception") + public void throwBindException() throws BindException { + BindException exception = new BindException(new Object(), "testObject"); + exception.reject("TestError", "Test binding error"); + throw exception; + } @PostMapping("/validated-list") - public void postValidatedList(@RequestBody List<@Valid ComplexValidatedObject> data) {} + public void postValidatedList(@RequestBody List<@Valid ComplexValidatedObject> data) { + // nothing to do + } @GetMapping("/complex-data") - public void dateFieldParam(ComplexData data) {} + public void dateFieldParam(ComplexData data) { + // nothing to do + } @GetMapping("/with-request-param") - public void get(@RequestParam("id") Long id) {} + public void get(@RequestParam("id") Long id) { + // nothing to do + } @PostMapping("/import") - public void uploadFile(@RequestParam("file") MultipartFile file) {} + public void uploadFile(@RequestParam("file") MultipartFile file) { + // nothing to do + } @GetMapping("/base-exception") public void throwsBaseException() { @@ -158,6 +186,34 @@ public static class ComplexData { private List nestedTestObjectList; } + @Getter + @Setter + @NoArgsConstructor + public static class ComplexValidatedFormObject { + + @NotNull + @NotBlank + private String name; + + @NotNull + private Integer age; + + @NotBlank + private String email; + } + + @Getter + @Setter + @NoArgsConstructor + public static class FormBindingObject { + + private String name; + + private Integer count; + + private LocalDate date; + } + @Getter @Setter @ToString diff --git a/src/test/java/ee/bitweb/core/object_mapper/ObjectMapperAutoConfigurationDisabledIntegrationTests.java b/src/test/java/ee/bitweb/core/object_mapper/ObjectMapperAutoConfigurationDisabledIntegrationTests.java new file mode 100644 index 0000000..5bb79e0 --- /dev/null +++ b/src/test/java/ee/bitweb/core/object_mapper/ObjectMapperAutoConfigurationDisabledIntegrationTests.java @@ -0,0 +1,32 @@ +package ee.bitweb.core.object_mapper; + +import ee.bitweb.core.TestSpringApplication; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ActiveProfiles; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("integration") +@ActiveProfiles("MockedInvokerTraceIdCreator") +@SpringBootTest( + classes = TestSpringApplication.class, + properties = { + "ee.bitweb.core.object-mapper.auto-configuration=false" + } +) +class ObjectMapperAutoConfigurationDisabledIntegrationTests { + + @Autowired + private ApplicationContext applicationContext; + + @Test + @DisplayName("Should not load ObjectMapperAutoConfiguration bean when disabled") + void shouldNotLoadAutoConfigurationBeanWhenDisabled() { + assertFalse(applicationContext.containsBean("objectMapperAutoConfiguration")); + } +} diff --git a/src/test/java/ee/bitweb/core/object_mapper/ObjectMapperAutoConfigurationIntegrationTests.java b/src/test/java/ee/bitweb/core/object_mapper/ObjectMapperAutoConfigurationIntegrationTests.java new file mode 100644 index 0000000..49424de --- /dev/null +++ b/src/test/java/ee/bitweb/core/object_mapper/ObjectMapperAutoConfigurationIntegrationTests.java @@ -0,0 +1,108 @@ +package ee.bitweb.core.object_mapper; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import ee.bitweb.core.TestSpringApplication; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ActiveProfiles; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("integration") +@ActiveProfiles("MockedInvokerTraceIdCreator") +@SpringBootTest( + classes = TestSpringApplication.class, + properties = { + "ee.bitweb.core.object-mapper.auto-configuration=true" + } +) +class ObjectMapperAutoConfigurationIntegrationTests { + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private ApplicationContext applicationContext; + + @Test + @DisplayName("Should load ObjectMapperAutoConfiguration bean when enabled") + void shouldLoadAutoConfigurationBeanWhenEnabled() { + assertTrue(applicationContext.containsBean("objectMapperAutoConfiguration")); + } + + @Test + @DisplayName("Should trim strings during deserialization") + void shouldTrimStringsDuringDeserialization() throws JsonProcessingException { + String json = "{\"value\": \" trimmed \"}"; + + TestDto result = objectMapper.readValue(json, TestDto.class); + + assertEquals("trimmed", result.value); + } + + @Test + @DisplayName("Should handle Java time types with JavaTimeModule") + void shouldHandleJavaTimeTypes() throws JsonProcessingException { + String json = "{\"timestamp\": \"2024-01-15T10:30:00Z\"}"; + + TestDtoWithTime result = objectMapper.readValue(json, TestDtoWithTime.class); + + assertNotNull(result.timestamp); + assertEquals(2024, result.timestamp.getYear()); + assertEquals(1, result.timestamp.getMonthValue()); + assertEquals(15, result.timestamp.getDayOfMonth()); + } + + @Test + @DisplayName("Should have ADJUST_DATES_TO_CONTEXT_TIME_ZONE disabled") + void shouldHaveAdjustDatesToContextTimeZoneDisabled() { + assertFalse(objectMapper.isEnabled(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)); + } + + @Test + @DisplayName("Should have ACCEPT_FLOAT_AS_INT disabled") + void shouldHaveAcceptFloatAsIntDisabled() { + assertFalse(objectMapper.isEnabled(DeserializationFeature.ACCEPT_FLOAT_AS_INT)); + } + + @Test + @DisplayName("Should reject float value for integer field when ACCEPT_FLOAT_AS_INT is disabled") + void shouldRejectFloatForIntegerField() { + String json = "{\"count\": 1.5}"; + + assertThrows(JsonProcessingException.class, () -> + objectMapper.readValue(json, TestDtoWithInt.class) + ); + } + + @Test + @DisplayName("Should preserve timezone in deserialized date") + void shouldPreserveTimezoneInDate() throws JsonProcessingException { + String json = "{\"timestamp\": \"2024-01-15T10:30:00+05:00\"}"; + + TestDtoWithTime result = objectMapper.readValue(json, TestDtoWithTime.class); + + assertEquals(ZoneOffset.ofHours(5), result.timestamp.getOffset()); + } + + static class TestDto { + public String value; + } + + static class TestDtoWithTime { + public OffsetDateTime timestamp; + } + + static class TestDtoWithInt { + public Integer count; + } +} diff --git a/src/test/java/ee/bitweb/core/object_mapper/ObjectMapperPropertiesTest.java b/src/test/java/ee/bitweb/core/object_mapper/ObjectMapperPropertiesTest.java new file mode 100644 index 0000000..454ceb8 --- /dev/null +++ b/src/test/java/ee/bitweb/core/object_mapper/ObjectMapperPropertiesTest.java @@ -0,0 +1,46 @@ +package ee.bitweb.core.object_mapper; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class ObjectMapperPropertiesTest { + + @Test + @DisplayName("Should have correct prefix constant") + void shouldHaveCorrectPrefixConstant() { + assertEquals("ee.bitweb.core.object-mapper", ObjectMapperProperties.PREFIX); + } + + @Test + @DisplayName("Should have default autoConfiguration as false") + void shouldHaveDefaultAutoConfigurationAsFalse() { + ObjectMapperProperties properties = new ObjectMapperProperties(); + + assertFalse(properties.getAutoConfiguration()); + } + + @Test + @DisplayName("Should allow setting autoConfiguration to true") + void shouldAllowSettingAutoConfigurationToTrue() { + ObjectMapperProperties properties = new ObjectMapperProperties(); + + properties.setAutoConfiguration(true); + + assertTrue(properties.getAutoConfiguration()); + } + + @Test + @DisplayName("Should allow setting autoConfiguration back to false") + void shouldAllowSettingAutoConfigurationBackToFalse() { + ObjectMapperProperties properties = new ObjectMapperProperties(); + properties.setAutoConfiguration(true); + + properties.setAutoConfiguration(false); + + assertFalse(properties.getAutoConfiguration()); + } +} diff --git a/src/test/java/ee/bitweb/core/object_mapper/deserializer/TrimmedStringDeserializerTest.java b/src/test/java/ee/bitweb/core/object_mapper/deserializer/TrimmedStringDeserializerTest.java new file mode 100644 index 0000000..08624db --- /dev/null +++ b/src/test/java/ee/bitweb/core/object_mapper/deserializer/TrimmedStringDeserializerTest.java @@ -0,0 +1,93 @@ +package ee.bitweb.core.object_mapper.deserializer; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Tag; +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 java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.*; + +@Tag("unit") +class TrimmedStringDeserializerTest { + + private ObjectMapper mapper; + + @BeforeEach + void setUp() { + mapper = new ObjectMapper(); + TrimmedStringDeserializer.addToObjectMapper(mapper); + } + + static Stream stringTrimmingCases() { + return Stream.of( + Arguments.of(" hello", "hello", "leading whitespace"), + Arguments.of("hello ", "hello", "trailing whitespace"), + Arguments.of(" hello world ", "hello world", "both leading and trailing whitespace"), + Arguments.of("hello world", "hello world", "internal whitespace preserved"), + Arguments.of(" ", "", "whitespace-only string"), + Arguments.of("", "", "empty string"), + Arguments.of("\t\nhello\t\n", "hello", "tabs and newlines") + ); + } + + @ParameterizedTest(name = "Should handle {2}") + @MethodSource("stringTrimmingCases") + void shouldTrimStrings(String input, String expected, String description) throws JsonProcessingException { + String json = "\"" + escapeJson(input) + "\""; + + String result = mapper.readValue(json, String.class); + + assertEquals(expected, result, description); + } + + @Test + @DisplayName("Should return null for null value") + void shouldReturnNullForNullValue() throws JsonProcessingException { + String result = mapper.readValue("null", String.class); + + assertNull(result); + } + + @Test + @DisplayName("Should trim string fields in object") + void shouldTrimStringFieldsInObject() throws JsonProcessingException { + String json = "{\"name\": \" John Doe \", \"email\": \" john@example.com \"}"; + + TestObject result = mapper.readValue(json, TestObject.class); + + assertAll( + () -> assertEquals("John Doe", result.name), + () -> assertEquals("john@example.com", result.email) + ); + } + + @Test + @DisplayName("Should trim strings in array") + void shouldTrimStringsInArray() throws JsonProcessingException { + String json = "[\" first \", \" second \", \" third \"]"; + + String[] result = mapper.readValue(json, String[].class); + + assertAll( + () -> assertEquals("first", result[0]), + () -> assertEquals("second", result[1]), + () -> assertEquals("third", result[2]) + ); + } + + private static String escapeJson(String input) { + return input.replace("\t", "\\t").replace("\n", "\\n"); + } + + static class TestObject { + public String name; + public String email; + } +}