From 2f2fd7513df1a4e2858723632cc58fdcfd4149d2 Mon Sep 17 00:00:00 2001
From: Jan Zickermann <jan.zickermann@dataport.de>
Date: Tue, 1 Apr 2025 10:52:17 +0200
Subject: [PATCH] KOP-2949 Catch RuntimeExceptions, defensively

---
 .../FormSolutionsEngineBasedAdapter.java      |  15 +-
 .../FormSolutionsEngineBasedAdapterTest.java  | 156 ++++++++++++++----
 2 files changed, 135 insertions(+), 36 deletions(-)

diff --git a/src/main/java/de/ozgcloud/eingang/semantik/enginebased/formsolutions/FormSolutionsEngineBasedAdapter.java b/src/main/java/de/ozgcloud/eingang/semantik/enginebased/formsolutions/FormSolutionsEngineBasedAdapter.java
index 5bfe77e2..80189430 100644
--- a/src/main/java/de/ozgcloud/eingang/semantik/enginebased/formsolutions/FormSolutionsEngineBasedAdapter.java
+++ b/src/main/java/de/ozgcloud/eingang/semantik/enginebased/formsolutions/FormSolutionsEngineBasedAdapter.java
@@ -33,9 +33,11 @@ import de.ozgcloud.eingang.common.formdata.FormData;
 import de.ozgcloud.eingang.common.formdata.FormDataUtils;
 import de.ozgcloud.eingang.semantik.enginebased.EngineBasedSemantikAdapter;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.log4j.Log4j2;
 
 @Component
 @RequiredArgsConstructor
+@Log4j2
 public class FormSolutionsEngineBasedAdapter implements EngineBasedSemantikAdapter {
 
 	public static final String IDENTIFIER_KEY = "identifier";
@@ -51,13 +53,22 @@ public class FormSolutionsEngineBasedAdapter implements EngineBasedSemantikAdapt
 		var processedFormData = formData;
 
 		for (var mapper : mappers) {
-			processedFormData = mapper.parseFormData(processedFormData);
+			processedFormData = applyFormDataMapperAndCatchException(mapper, processedFormData);
 		}
 
 		return removeProcessedData(processedFormData);
 	}
 
-	protected FormData removeProcessedData(FormData formData) {
+	private FormData applyFormDataMapperAndCatchException(FormSolutionsEngineBasedMapper mapper, FormData formData) {
+		try {
+			return mapper.parseFormData(formData);
+		} catch (RuntimeException e) {
+			LOG.error("Error while parsing form data with mapper {}", mapper.getClass().getSimpleName(), e);
+			return formData;
+		}
+	}
+
+	FormData removeProcessedData(FormData formData) {
 		return FormDataUtils.from(formData)
 				.remove(ASSISTANT)
 				.remove(ANLIEGEN_ID)
diff --git a/src/test/java/de/ozgcloud/eingang/semantik/enginebased/formsolutions/FormSolutionsEngineBasedAdapterTest.java b/src/test/java/de/ozgcloud/eingang/semantik/enginebased/formsolutions/FormSolutionsEngineBasedAdapterTest.java
index 4e5a07d9..cd2dce9d 100644
--- a/src/test/java/de/ozgcloud/eingang/semantik/enginebased/formsolutions/FormSolutionsEngineBasedAdapterTest.java
+++ b/src/test/java/de/ozgcloud/eingang/semantik/enginebased/formsolutions/FormSolutionsEngineBasedAdapterTest.java
@@ -29,7 +29,7 @@ import static org.assertj.core.api.Assertions.*;
 import static org.mockito.ArgumentMatchers.*;
 import static org.mockito.Mockito.*;
 
-import java.util.Collections;
+import java.util.List;
 import java.util.Map;
 
 import org.junit.jupiter.api.BeforeEach;
@@ -42,7 +42,6 @@ import org.mockito.Spy;
 import org.springframework.test.util.ReflectionTestUtils;
 
 import de.ozgcloud.eingang.common.formdata.FormData;
-import de.ozgcloud.eingang.common.formdata.FormDataTestFactory;
 
 class FormSolutionsEngineBasedAdapterTest {
 
@@ -50,73 +49,162 @@ class FormSolutionsEngineBasedAdapterTest {
 	@InjectMocks
 	private FormSolutionsEngineBasedAdapter adapter;
 	@Mock
-	private FormSolutionsEngineBasedMapper mapper;
+	private FormSolutionsEngineBasedMapper mapper0;
+
+	@Mock
+	private FormSolutionsEngineBasedMapper mapper1;
 
 	@DisplayName("Parse formData")
 	@Nested
 	class TestParseFormData {
 
-		private final FormData formData = FormDataTestFactory.create();
+		@Mock
+		private FormData formData0;
+
+		@Mock
+		private FormData formData1;
+
+		@Mock
+		private FormData formData2;
+
+		@Mock
+		private FormData formData3;
 
 		@BeforeEach
 		void mockMappers() {
-			ReflectionTestUtils.setField(adapter, "mappers", Collections.singletonList(mapper));
+			ReflectionTestUtils.setField(adapter, "mappers", List.of(mapper0, mapper1));
 		}
 
-		@Test
-		void shouldCallMappers() {
-			when(mapper.parseFormData(any())).thenReturn(formData);
+		@DisplayName("without exception")
+		@Nested
+		class TestWithoutException {
 
-			adapter.parseFormData(formData);
+			@BeforeEach
+			void mock() {
+				when(mapper0.parseFormData(any())).thenReturn(formData1);
+				when(mapper1.parseFormData(any())).thenReturn(formData2);
+				doReturn(formData3).when(adapter).removeProcessedData(any());
+			}
 
-			verify(mapper).parseFormData(formData);
-		}
+			@DisplayName("should call first mapper")
+			@Test
+			void shouldCallFirstMapper() {
+				parseFormData();
 
-		@Test
-		void shouldCallRemoveProcessedRawData() {
-			when(mapper.parseFormData(any())).thenReturn(formData);
+				verify(mapper0).parseFormData(formData0);
+			}
 
-			adapter.parseFormData(formData);
+			@DisplayName("should call second mapper")
+			@Test
+			void shouldCallSecondMapper() {
+				parseFormData();
+
+				verify(mapper1).parseFormData(formData1);
+			}
+
+			@DisplayName("should call removeProcessedData")
+			@Test
+			void shouldCallRemoveProcessedData() {
+				parseFormData();
+
+				verify(adapter).removeProcessedData(formData2);
+			}
+
+			@DisplayName("should return")
+			@Test
+			void shouldReturn() {
+				var result = parseFormData();
+
+				assertThat(result).isEqualTo(formData3);
+			}
 
-			verify(adapter).removeProcessedData(formData);
 		}
 
-		@DisplayName("remove processed data")
+		@DisplayName("with exception")
 		@Nested
-		class TestRemoveProcessedData {
+		class TestWithException {
 
-			private final Map<String, Object> formDataMap = Map.of(ASSISTANT, "testValue",
-					ANLIEGEN_ID, "testValue2", KOMMUNALVERWALTUNG_ID, "testValue3",
-					POSTKORBHANDLE, "testValue4");
-			private final FormData formData = FormData.builder().formData(formDataMap).build();
+			@BeforeEach
+			void mock() {
+				when(mapper0.parseFormData(any())).thenThrow(new RuntimeException());
+				when(mapper1.parseFormData(any())).thenReturn(formData1);
+				doReturn(formData2).when(adapter).removeProcessedData(any());
+			}
 
+			@DisplayName("should call first mapper")
 			@Test
-			void shouldRemoveAssistant() {
-				var cleanedFormData = adapter.removeProcessedData(formData);
+			void shouldCallFirstMapper() {
+				parseFormData();
 
-				assertThat(cleanedFormData.getFormData()).doesNotContainKey(ASSISTANT);
+				verify(mapper0).parseFormData(formData0);
 			}
 
+			@DisplayName("should call second mapper with same data")
 			@Test
-			void shouldRemoveAnliegenId() {
-				var cleanedFormData = adapter.removeProcessedData(formData);
+			void shouldCallSecondMapperWithSameData() {
+				parseFormData();
 
-				assertThat(cleanedFormData.getFormData()).doesNotContainKey(ANLIEGEN_ID);
+				verify(mapper1).parseFormData(formData0);
 			}
 
+			@DisplayName("should call removeProcessedData")
 			@Test
-			void shouldRemoveKommunalVerwaltungId() {
-				var cleanedFormData = adapter.removeProcessedData(formData);
+			void shouldCallRemoveProcessedData() {
+				parseFormData();
 
-				assertThat(cleanedFormData.getFormData()).doesNotContainKey(KOMMUNALVERWALTUNG_ID);
+				verify(adapter).removeProcessedData(formData1);
 			}
 
+			@DisplayName("should return")
 			@Test
-			void shouldRemovePostkorbhandle() {
-				var cleanedFormData = adapter.removeProcessedData(formData);
+			void shouldReturn() {
+				var result = parseFormData();
 
-				assertThat(cleanedFormData.getFormData()).doesNotContainKey(POSTKORBHANDLE);
+				assertThat(result).isEqualTo(formData2);
 			}
+
+		}
+
+		FormData parseFormData() {
+			return adapter.parseFormData(formData0);
+		}
+	}
+
+	@DisplayName("remove processed data")
+	@Nested
+	class TestRemoveProcessedData {
+
+		private final Map<String, Object> formDataMap = Map.of(ASSISTANT, "testValue",
+				ANLIEGEN_ID, "testValue2", KOMMUNALVERWALTUNG_ID, "testValue3",
+				POSTKORBHANDLE, "testValue4");
+		private final FormData formData = FormData.builder().formData(formDataMap).build();
+
+		@Test
+		void shouldRemoveAssistant() {
+			var cleanedFormData = adapter.removeProcessedData(formData);
+
+			assertThat(cleanedFormData.getFormData()).doesNotContainKey(ASSISTANT);
+		}
+
+		@Test
+		void shouldRemoveAnliegenId() {
+			var cleanedFormData = adapter.removeProcessedData(formData);
+
+			assertThat(cleanedFormData.getFormData()).doesNotContainKey(ANLIEGEN_ID);
+		}
+
+		@Test
+		void shouldRemoveKommunalVerwaltungId() {
+			var cleanedFormData = adapter.removeProcessedData(formData);
+
+			assertThat(cleanedFormData.getFormData()).doesNotContainKey(KOMMUNALVERWALTUNG_ID);
+		}
+
+		@Test
+		void shouldRemovePostkorbhandle() {
+			var cleanedFormData = adapter.removeProcessedData(formData);
+
+			assertThat(cleanedFormData.getFormData()).doesNotContainKey(POSTKORBHANDLE);
 		}
 	}
 }
\ No newline at end of file
-- 
GitLab