diff --git a/pom.xml b/pom.xml index d2aa4adce675a926c1252187e4a91a32638fba57..1dfe0ccae59004024c98755870c672212a77d4c0 100644 --- a/pom.xml +++ b/pom.xml @@ -45,7 +45,7 @@ <properties> <eingang-manager.version>2.20.0</eingang-manager.version> - <formsolutions-semantik.version>2.20.0</formsolutions-semantik.version> + <formsolutions-semantik.version>2.21.0-SNAPSHOT</formsolutions-semantik.version> <jaxb3-plugin.version>0.15.0</jaxb3-plugin.version> <xmlschema.version>2.3.0</xmlschema.version> diff --git a/src/main/java/de/ozgcloud/eingang/formsolutions/FormSolutionsEingang.java b/src/main/java/de/ozgcloud/eingang/formsolutions/FormSolutionsEingang.java index 9cf795bc8eed4ad8f8b390bf339aa508f620b318..bbc3998c8ebfe39216aa566150b7b60b8635d2cf 100644 --- a/src/main/java/de/ozgcloud/eingang/formsolutions/FormSolutionsEingang.java +++ b/src/main/java/de/ozgcloud/eingang/formsolutions/FormSolutionsEingang.java @@ -24,9 +24,7 @@ package de.ozgcloud.eingang.formsolutions; import java.io.File; -import java.util.Map; -import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import de.ozgcloud.common.binaryfile.FileDataDeserializer; @@ -37,17 +35,7 @@ import lombok.extern.jackson.Jacksonized; @Getter @Builder @Jacksonized -public class FormSolutionsEingang { - - private Map<String, Object> assistant; - - private String postkorbhandle; - private String kommunalverwaltungId; - private String transactionId; - private String zustaendigeStelle; - @JsonProperty("gemeindeschlüssel") - private String gemeindeSchluessel; - private String anliegenId; +class FormSolutionsEingang { @JsonDeserialize(using = FileDataDeserializer.class) private File pdf; diff --git a/src/main/java/de/ozgcloud/eingang/formsolutions/FormSolutionsRequestMapper.java b/src/main/java/de/ozgcloud/eingang/formsolutions/FormSolutionsRequestMapper.java index 9ce665fdc5ffda93836d3266e8b1d32971117b30..c62262d90f231c1f1d9b91b23094fcec72ff889b 100644 --- a/src/main/java/de/ozgcloud/eingang/formsolutions/FormSolutionsRequestMapper.java +++ b/src/main/java/de/ozgcloud/eingang/formsolutions/FormSolutionsRequestMapper.java @@ -26,8 +26,6 @@ package de.ozgcloud.eingang.formsolutions; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; import java.util.Map; import java.util.Objects; import java.util.Optional; @@ -36,6 +34,7 @@ import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import de.ozgcloud.common.errorhandling.TechnicalException; @@ -52,13 +51,14 @@ class FormSolutionsRequestMapper { static final String FILE_NAME_JSON_REPRESENTATION = "form-data.json"; static final String FILE_NAME_PDF_REPRESENTATION = "eingang.pdf"; - static final String FORMDATA_FIELD_ZUSTAENDIGE_STELLE = "zustaendigeStelle"; - public static final String FORMDATA_FIELD_ASSISTANT = "assistant"; - public static final String FORMDATA_FIELD_POSTKORBHANDLE = "postkorbhandle"; - static final String FORMDATA_FIELD_TRANSACTION_ID = "transactionId"; - private final FormSolutionsAttachmentsMapper attachmentMapper; - private final ObjectMapper objectMapper; + private final ObjectMapper objectMapper = createObjectMapper(); + + static ObjectMapper createObjectMapper() { + var obj = new ObjectMapper(); + obj.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + return obj; + } public FormData map(File jsonFile) { var eingang = mapEingang(jsonFile); @@ -68,7 +68,6 @@ class FormSolutionsRequestMapper { FormData buildFormData(File jsonFile, FormSolutionsEingang eingang) { var builder = FormData.builder() - .formData(buildFormDataMap(eingang)) .attachments(attachmentMapper.mapAttachments(eingang.getZip())) .representation(buildJsonFile(jsonFile)); var numberOfRepresentations = 1; @@ -97,23 +96,6 @@ class FormSolutionsRequestMapper { .build(); } - Map<String, Object> buildFormDataMap(FormSolutionsEingang eingang) { - Map<String, Object> map = new HashMap<>(); - addIfValueNotNull(map, FORMDATA_FIELD_ASSISTANT, eingang.getAssistant()); - addIfValueNotNull(map, FORMDATA_FIELD_POSTKORBHANDLE, eingang.getPostkorbhandle()); - addIfValueNotNull(map, FORMDATA_FIELD_TRANSACTION_ID, eingang.getTransactionId()); - addIfValueNotNull(map, FORMDATA_FIELD_ZUSTAENDIGE_STELLE, eingang.getZustaendigeStelle()); - - return Collections.unmodifiableMap(map); - } - - private Map<String, Object> addIfValueNotNull(Map<String, Object> map, String key, Object value) { - if (Objects.nonNull(value)) { - map.put(key, value); - } - return map; - } - FormSolutionsEingang mapEingang(File jsonFile) { try (var in = new FileInputStream(jsonFile)) { return objectMapper.readValue(in, FormSolutionsEingang.class); diff --git a/src/main/java/de/ozgcloud/eingang/formsolutions/SendFormEndpoint.java b/src/main/java/de/ozgcloud/eingang/formsolutions/SendFormEndpoint.java index d00eea0b410bf83899af1b67c127466a27f8eef8..9bfae360d9fcffb2fd11aa2c1ff2b590f30e28c0 100644 --- a/src/main/java/de/ozgcloud/eingang/formsolutions/SendFormEndpoint.java +++ b/src/main/java/de/ozgcloud/eingang/formsolutions/SendFormEndpoint.java @@ -27,8 +27,6 @@ import java.io.File; import java.util.UUID; import java.util.function.Supplier; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.logging.log4j.CloseableThreadContext; import org.springframework.ws.server.endpoint.annotation.Endpoint; @@ -38,6 +36,8 @@ import org.springframework.ws.server.endpoint.annotation.ResponsePayload; import de.ozgcloud.common.binaryfile.TempFileUtils; import de.ozgcloud.eingang.semantik.SemantikAdapter; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; @Endpoint @Log4j2 @@ -57,7 +57,7 @@ public class SendFormEndpoint { return doSurroundOn(() -> handleRequest(request)); } - private Response handleRequest(Request request) { + Response handleRequest(Request request) { try { semantikAdapter.processFormData(requestMapper.map(writeRequestJsonToFile(request.getJSON()))); return buildSuccessResponse(); diff --git a/src/test/java/de/ozgcloud/eingang/formsolutions/FormSolutionsEingangTestFactory.java b/src/test/java/de/ozgcloud/eingang/formsolutions/FormSolutionsEingangTestFactory.java deleted file mode 100644 index 9a4be9a1769ee2b0c346e17d73b0dd4df460c465..0000000000000000000000000000000000000000 --- a/src/test/java/de/ozgcloud/eingang/formsolutions/FormSolutionsEingangTestFactory.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2024 Das Land Schleswig-Holstein vertreten durch den - * Ministerpräsidenten des Landes Schleswig-Holstein - * Staatskanzlei - * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung - * - * Lizenziert unter der EUPL, Version 1.2 oder - sobald - * diese von der Europäischen Kommission genehmigt wurden - - * Folgeversionen der EUPL ("Lizenz"); - * Sie dürfen dieses Werk ausschließlich gemäß - * dieser Lizenz nutzen. - * Eine Kopie der Lizenz finden Sie hier: - * - * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * Sofern nicht durch anwendbare Rechtsvorschriften - * gefordert oder in schriftlicher Form vereinbart, wird - * die unter der Lizenz verbreitete Software "so wie sie - * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - - * ausdrücklich oder stillschweigend - verbreitet. - * Die sprachspezifischen Genehmigungen und Beschränkungen - * unter der Lizenz sind dem Lizenztext zu entnehmen. - */ -package de.ozgcloud.eingang.formsolutions; - -import static de.ozgcloud.eingang.common.formdata.FormSolutionsTestFactory.*; - -import java.util.Map; - -public class FormSolutionsEingangTestFactory { - - public static FormSolutionsEingang create() { - return createBuilder().build(); - } - - public static FormSolutionsEingang.FormSolutionsEingangBuilder createBuilder() { - return FormSolutionsEingang.builder() - .assistant(Map.of()) - .zustaendigeStelle(ZUSTAENDIGE_STELLE) - .postkorbhandle(POSTFACH_ID_STELLE) - .transactionId(FORM_ID_VALUE); - - } -} diff --git a/src/test/java/de/ozgcloud/eingang/formsolutions/FormSolutionsRequestMapperITCase.java b/src/test/java/de/ozgcloud/eingang/formsolutions/FormSolutionsRequestMapperITCase.java deleted file mode 100644 index dbacda8a8aae670992e1084fd29bdc6ead0df799..0000000000000000000000000000000000000000 --- a/src/test/java/de/ozgcloud/eingang/formsolutions/FormSolutionsRequestMapperITCase.java +++ /dev/null @@ -1,207 +0,0 @@ -/* - * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den - * Ministerpräsidenten des Landes Schleswig-Holstein - * Staatskanzlei - * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung - * - * Lizenziert unter der EUPL, Version 1.2 oder - sobald - * diese von der Europäischen Kommission genehmigt wurden - - * Folgeversionen der EUPL ("Lizenz"); - * Sie dürfen dieses Werk ausschließlich gemäß - * dieser Lizenz nutzen. - * Eine Kopie der Lizenz finden Sie hier: - * - * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 - * - * Sofern nicht durch anwendbare Rechtsvorschriften - * gefordert oder in schriftlicher Form vereinbart, wird - * die unter der Lizenz verbreitete Software "so wie sie - * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - - * ausdrücklich oder stillschweigend - verbreitet. - * Die sprachspezifischen Genehmigungen und Beschränkungen - * unter der Lizenz sind dem Lizenztext zu entnehmen. - */ -package de.ozgcloud.eingang.formsolutions; - -import static de.ozgcloud.eingang.common.formdata.FormSolutionsTestFactory.*; -import static org.assertj.core.api.Assertions.assertThat; - -import java.io.InputStream; -import java.util.List; -import java.util.Map; - -import lombok.SneakyThrows; -import org.assertj.core.api.ObjectAssert; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.test.context.ActiveProfiles; -import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; - -import de.ozgcloud.common.binaryfile.TempFileUtils; -import de.ozgcloud.common.test.TestUtils; -import de.ozgcloud.eingang.common.formdata.FormData; -import de.ozgcloud.eingang.common.formdata.IncomingFile; -import de.ozgcloud.eingang.common.formdata.IncomingFileGroup; -import de.ozgcloud.eingang.semantik.enginebased.formsolutions.FormSolutionsEngineBasedAdapter; - - -@SpringBootTest -@ActiveProfiles({ "local", "itcase" }) -class FormSolutionsRequestMapperITCase { - - private static final String COMPONENTS = "components"; - private static final String PANELS = "panels"; - - @MockitoSpyBean - private FormSolutionsRequestMapper mapper; - - @Nested - class TestParseExampleData { - @Test - void shouldParseData() { - var parsed = parseRequestData(SIMPLE_JSON_DATA); - - assertThat(parsed).isNotNull(); - } - - @Test - void shouldContainRawData() { - var parsed = parseRequestData(SIMPLE_JSON_DATA); - - assertThat(parsed.getFormData()).isNotNull(); - } - - @Nested - class TestWithPanels { - @Test - void shouldContainPanel() { - var parsed = parseAndGetPanel(); - - assertThat(parsed).isNotNull(); - } - - @Test - void shouldHaveIdentifier() { - var panel = parseAndGetPanel(); - - assertThat(panel.get(FormSolutionsEngineBasedAdapter.IDENTIFIER_KEY)).isNotNull(); - } - - @Test - @SuppressWarnings("unchecked") - void shouldHaveComponents() { - var panel = parseAndGetPanel(); - var components = (List<Map<String, Object>>) panel.get(COMPONENTS); - - assertThat(components).hasSize(2); - } - } - - private Map<String, Object> parseAndGetPanel() { - return FormSolutionsRequestMapperITCase.this.parseAndGetPanel(SIMPLE_JSON_DATA); - } - } - - @Nested - class TestParseNestedComponents { - - @Test - void shouldParseData() { - var parsed = parseRequestData(NESTED_COMPONENTS_JSON); - - assertThat(parsed).isNotNull(); - } - - @Test - @SuppressWarnings("unchecked") - void shouldHaveComponents() { - var panel = parseAndGetPanel(); - - assertThat((List<Map<String, Object>>) panel.get(COMPONENTS)).hasSize(1); - } - - @Test - void shouldHaveIdentifier() { - var component = parseAndGetComponent(); - - assertThat(component).containsEntry(FormSolutionsEngineBasedAdapter.IDENTIFIER_KEY, OBJEKTGRUPPE_0); - } - - @Test - void shouldHaveNestedComponents() { - var component = parseAndGetComponent(); - - assertThat(component.keySet()).hasSize(3); - } - - @SuppressWarnings("unchecked") - private Map<String, Object> parseAndGetComponent() { - return ((List<Map<String, Object>>) parseAndGetPanel().get(COMPONENTS)).get(0); - } - - private Map<String, Object> parseAndGetPanel() { - return FormSolutionsRequestMapperITCase.this.parseAndGetPanel(NESTED_COMPONENTS_JSON); - } - - } - - @Nested - class TestParsePdfRepresentation { - - @Test - void shouldHaveRepresentations() { - var parsed = parseRequestData(PDF_REPRESENTATION_JSON); - - assertThat(parsed.getRepresentations()).hasSize(2); - } - - @Test - @SneakyThrows - void shouldHavePdf() { - var parsed = parseRequestData(PDF_REPRESENTATION_JSON); - - ObjectAssert<IncomingFile> firstRepresentationAssert = assertThat(parsed.getRepresentations()) - .filteredOn(inFile -> inFile.getContentType().equals("application/pdf")).singleElement(); - firstRepresentationAssert.extracting(IncomingFile::getName).isEqualTo("eingang.pdf"); - firstRepresentationAssert.extracting(IncomingFile::getContentStream).extracting(stream -> toArray(stream)) - .isEqualTo(PDF_VALUE_DECODED.getBytes()); - } - } - - @Nested - class TestParseAttachmentZip { - - @Test - @SneakyThrows - void shouldHaveZip() { - var parsed = parseRequestData(ZIP_ATTACHMENT_JSON); - - ObjectAssert<IncomingFileGroup> firstAttachmentAssert = assertThat(parsed.getAttachments()).hasSize(1).first(); - firstAttachmentAssert.extracting(IncomingFileGroup::getName).isEqualTo(FormSolutionsAttachmentsMapper.FILE_GROUP_ZIP_NAME); - var attachmentFileAssert = firstAttachmentAssert.extracting(fileGroup -> fileGroup.getFiles().get(0)); - attachmentFileAssert.extracting(IncomingFile::getName).isEqualTo("attachments.zip"); - attachmentFileAssert.extracting(file -> toArray(file.getContentStream())).isEqualTo(ZIP_VALUE_DECODED.getBytes()); - } - - } - - // TODO remove this method when TestUtils is not throwing Exception anymore. - @SneakyThrows - private byte[] toArray(InputStream stream) { - return TestUtils.contentStreamToByteArray(stream); - } - - @SuppressWarnings("unchecked") - private Map<String, Object> parseAndGetPanel(String json) { - var data = (Map<String, Object>) parseRequestData(json).getFormData().get(FormSolutionsEngineBasedAdapter.ASSISTANT); - return ((List<Map<String, Object>>) data.get(PANELS)).get(0); - } - - private FormData parseRequestData(String json) { - var file = TempFileUtils.writeTmpFile(json); - var result = mapper.map(file); - file.delete(); - return result; - } -} diff --git a/src/test/java/de/ozgcloud/eingang/formsolutions/FormSolutionsRequestMapperTest.java b/src/test/java/de/ozgcloud/eingang/formsolutions/FormSolutionsRequestMapperTest.java index d8377e6a1db3e002948dc32c1c35a8b7b6179be7..b5627d9b795710d4e8612f830c41a0cd69bc822a 100644 --- a/src/test/java/de/ozgcloud/eingang/formsolutions/FormSolutionsRequestMapperTest.java +++ b/src/test/java/de/ozgcloud/eingang/formsolutions/FormSolutionsRequestMapperTest.java @@ -25,16 +25,12 @@ package de.ozgcloud.eingang.formsolutions; import static de.ozgcloud.eingang.common.formdata.FormSolutionsTestFactory.*; import static de.ozgcloud.eingang.formsolutions.FormSolutionsRequestMapper.*; -import static de.ozgcloud.eingang.semantik.enginebased.formsolutions.FormSolutionsEngineBasedAdapter.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.io.File; -import java.io.InputStream; -import java.util.Collections; import java.util.List; -import java.util.Map; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -48,23 +44,15 @@ import org.mockito.Mock; import org.mockito.Spy; import org.springframework.http.MediaType; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; - import de.ozgcloud.common.binaryfile.TempFileUtils; import de.ozgcloud.common.errorhandling.TechnicalException; import de.ozgcloud.eingang.common.formdata.FormData; import de.ozgcloud.eingang.common.formdata.IncomingFile; import de.ozgcloud.eingang.common.formdata.IncomingFileGroup; import de.ozgcloud.eingang.common.formdata.IncomingFileGroupTestFactory; -import lombok.SneakyThrows; class FormSolutionsRequestMapperTest { - private static final String COMPONENTS = "components"; - private static final String STRING_VALUE = "stringValue"; - private static final String PANELS = "panels"; - @Spy @InjectMocks private FormSolutionsRequestMapper mapper; @@ -72,15 +60,35 @@ class FormSolutionsRequestMapperTest { @Mock private FormSolutionsAttachmentsMapper attachmentMapper; - @Spy - private ObjectMapper objectMapper = new ObjectMapper(); - private File simpleJsonFile; private File nestedComponenetJsonFile; @BeforeEach void writeJsonFile() { - simpleJsonFile = TempFileUtils.writeTmpFile(SIMPLE_JSON_DATA); + simpleJsonFile = TempFileUtils.writeTmpFile(""" + { + "assistant": { + "identifier": "AS_123", + "panels": [{ + "identifier": "Textfeld (einzeilig)", + "components": [{ + "identifier": "Objektgruppe[0]", + "needed": true, + "components": [{ + "identifier": "Datums- / Uhrzeitfeld", + "needed": true, + "stringValue": "22.05.1996" + }] + }] + }] + }, + "pdf": "TG9yZW0gaXBzdW0=", + "zip": "TG9yZW0gaXBzdW0=", + "zustaendigeStelle": "%s", + "postkorbhandle": "%s", + "transactionId": "%s" + } + """.formatted(ZUSTAENDIGE_STELLE, POSTFACH_ID_STELLE, FORM_ID_VALUE)); nestedComponenetJsonFile = TempFileUtils.writeTmpFile(NESTED_COMPONENTS_JSON); } @@ -93,7 +101,8 @@ class FormSolutionsRequestMapperTest { @DisplayName("map") @Nested class TestMap { - private final FormSolutionsEingang eingang = FormSolutionsEingangTestFactory.create(); + @Mock + private FormSolutionsEingang eingang; @BeforeEach void mock() { @@ -145,28 +154,11 @@ class FormSolutionsRequestMapperTest { void mock() { when(eingang.getZip()).thenReturn(zipFile); - doReturn(Map.of(IDENTIFIER_KEY, STRING_VALUE)).when(mapper).buildFormDataMap(any()); when(attachmentMapper.mapAttachments(any())).thenReturn(List.of(attachmentGroup)); doReturn(jsonIncomingFile).when(mapper).buildJsonFile(any()); doReturn(formDataControl).when(mapper).buildFormDataControl(); } - @DisplayName("should call buildFormDataMap") - @Test - void shouldCallBuildFormDataMap() { - buildFormData(); - - verify(mapper).buildFormDataMap(eingang); - } - - @DisplayName("should return formData") - @Test - void shouldReturnFormData() { - var result = buildFormData(); - - assertThat(result.getFormData()).isEqualTo(Map.of(IDENTIFIER_KEY, STRING_VALUE)); - } - @DisplayName("should call mapAttachments") @Test void shouldCallMapAttachments() { @@ -356,113 +348,37 @@ class FormSolutionsRequestMapperTest { } + @DisplayName("map eingang") @Nested - class TestJsonToEingangMapping { + class TestMapEingang { + @DisplayName("should return zip") @Test - void shouldMapControlValues() { - var eingang = mapper.mapEingang(simpleJsonFile); + void shouldReturn() { + var result = mapEingang(); - assertThat(eingang).isNotNull().usingRecursiveComparison() - .ignoringFields("zip", "pdf", "assistant").isEqualTo(FormSolutionsEingangTestFactory.create()); + assertThat(result.getZip().length()).isGreaterThan(0); } + @DisplayName("should return pdf") @Test - void shouldHaveAssistantData() { - var eingang = mapper.mapEingang(simpleJsonFile); + void shouldReturnPdf() { + var result = mapEingang(); - assertThat(eingang.getAssistant()).isNotEmpty(); + assertThat(result.getPdf().length()).isGreaterThan(0); } + @DisplayName("should throw with bad json") @Test - @SneakyThrows - void shouldHandleJsonException() throws JsonProcessingException { - doThrow(JsonProcessingException.class).when(objectMapper).readValue(any(InputStream.class), eq(FormSolutionsEingang.class)); + void shouldThrowWithBadJson() { + var badJsonFile = TempFileUtils.writeTmpFile("{ \"zustaendigeStell..."); - assertThatThrownBy(() -> mapper.mapEingang(simpleJsonFile)).isInstanceOf(TechnicalException.class); + assertThatThrownBy(() -> mapper.mapEingang(badJsonFile)) + .isInstanceOf(TechnicalException.class); } - @Test - void shouldContainFormIdentifier() { - var eingang = mapper.mapEingang(simpleJsonFile); - - assertThat(eingang.getAssistant()).containsEntry(IDENTIFIER_KEY, IDENTIFIER_VALUE); - } - - @Nested - class TestPanels { - @Test - void shouldContainPanels() { - var eingang = mapper.mapEingang(simpleJsonFile); - - assertThat(getPanels(eingang)).isNotNull(); - } - - @Test - void shouldContainPanelIdentifier() { - var eingang = mapper.mapEingang(simpleJsonFile); - - assertThat(getPanels(eingang).getFirst()).containsEntry(IDENTIFIER_KEY, PANEL_ID); - } - - @Test - void shouldContainPanelComponets() { - var eingang = mapper.mapEingang(simpleJsonFile); - - assertThat(getPanels(eingang).getFirst().get(COMPONENTS)).isNotNull(); - } - - @Test - void shouldContainTextComponets() { - var eingang = mapper.mapEingang(simpleJsonFile); - - assertThat(getComponents(eingang).getFirst()) - .containsEntry(IDENTIFIER_KEY, COMPONENT_ID) - .containsEntry(STRING_VALUE, COMPONENT_VALUE); - } - - @Test - void shouldContainDateComponets() { - var eingang = mapper.mapEingang(simpleJsonFile); - - assertThat(getComponents(eingang).get(1)) - .containsEntry(IDENTIFIER_KEY, DATE_COMPONENT_ID) - .containsEntry(STRING_VALUE, DATE_COMPONENT_VALUE); - } - - @Nested - class TestNestedPanels { - @Test - void shouldContainGroup() { - var eingang = mapper.mapEingang(nestedComponenetJsonFile); - - assertThat(getComponents(eingang).getFirst()).containsEntry(IDENTIFIER_KEY, OBJEKTGRUPPE_0); - } - - @Test - void shouldContainDateField() { - var eingang = mapper.mapEingang(nestedComponenetJsonFile); - - assertThat(getNestedComponents(eingang).getFirst()) - .containsEntry(IDENTIFIER_KEY, DATE_COMPONENT_ID) - .containsEntry(STRING_VALUE, DATE_COMPONENT_VALUE); - } - } - } - - @SuppressWarnings("unchecked") - private List<Map<String, Object>> getComponents(FormSolutionsEingang eingang) { - return (List<Map<String, Object>>) getPanels(eingang).getFirst().get(COMPONENTS); - } - - @SuppressWarnings("unchecked") - private List<Map<String, Object>> getNestedComponents(FormSolutionsEingang eingang) { - return (List<Map<String, Object>>) ((List<Map<String, Object>>) getPanels(eingang).getFirst().get(COMPONENTS)).getFirst().get(COMPONENTS); - } - - @SuppressWarnings("unchecked") - private List<Map<String, Object>> getPanels(FormSolutionsEingang eingang) { - return (List<Map<String, Object>>) eingang.getAssistant().getOrDefault(PANELS, Collections.emptyList()); + FormSolutionsEingang mapEingang() { + return mapper.mapEingang(simpleJsonFile); } } @@ -529,29 +445,4 @@ class FormSolutionsRequestMapperTest { } - @Nested - class TestBuildFormDataMap { - - @Test - void shouldHavePostkorbHandle() { - var formData = mapper.buildFormDataMap(FormSolutionsEingangTestFactory.create()); - - assertThat(formData).containsEntry(FORMDATA_FIELD_POSTKORBHANDLE, POSTFACH_ID_STELLE); - } - - @Test - void shouldHaveZustaendigeStelle() { - var formData = mapper.buildFormDataMap(FormSolutionsEingangTestFactory.create()); - - assertThat(formData).containsKey(FORMDATA_FIELD_ZUSTAENDIGE_STELLE); - } - - @Test - void shouldHaveTransactionId() { - var formData = mapper.buildFormDataMap(FormSolutionsEingangTestFactory.create()); - - assertThat(formData).containsKey(FORMDATA_FIELD_TRANSACTION_ID); - } - } - } diff --git a/src/test/java/de/ozgcloud/eingang/formsolutions/FormsolutionsITCase.java b/src/test/java/de/ozgcloud/eingang/formsolutions/FormsolutionsITCase.java index f3db3f6d8747b99621ddfab5e59334976991a8dd..47995be5596ae718e39b58cc2c09fbe4aeba9b73 100644 --- a/src/test/java/de/ozgcloud/eingang/formsolutions/FormsolutionsITCase.java +++ b/src/test/java/de/ozgcloud/eingang/formsolutions/FormsolutionsITCase.java @@ -23,38 +23,30 @@ */ package de.ozgcloud.eingang.formsolutions; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; +import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; -import java.util.List; +import java.util.Map; -import io.grpc.Channel; -import io.grpc.stub.CallStreamObserver; -import io.grpc.stub.ClientResponseObserver; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Captor; -import org.mockito.Mock; -import org.mockito.verification.Timeout; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.bean.override.mockito.MockitoBean; -import org.springframework.test.util.ReflectionTestUtils; import de.ozgcloud.common.test.TestUtils; -import de.ozgcloud.eingang.router.ManagableStub; -import de.ozgcloud.eingang.router.VorgangManagerServerResolver; -import de.ozgcloud.vorgang.grpc.binaryFile.BinaryFileServiceGrpc.BinaryFileServiceStub; -import de.ozgcloud.vorgang.grpc.binaryFile.GrpcUploadBinaryFileRequest; -import de.ozgcloud.vorgang.grpc.binaryFile.GrpcUploadBinaryFileResponse; -import de.ozgcloud.vorgang.vorgang.GrpcCreateVorgangRequest; -import de.ozgcloud.vorgang.vorgang.GrpcCreateVorgangResponse; -import de.ozgcloud.vorgang.vorgang.VorgangServiceGrpc.VorgangServiceBlockingStub; +import de.ozgcloud.eingang.common.formdata.FormData; +import de.ozgcloud.eingang.common.formdata.IncomingFile; +import de.ozgcloud.eingang.common.formdata.IncomingFileGroup; +import de.ozgcloud.eingang.common.formdata.ServiceKonto; +import de.ozgcloud.eingang.common.formdata.ZustaendigeStelle; +import de.ozgcloud.eingang.router.VorgangService; @SpringBootTest @DirtiesContext @@ -65,121 +57,81 @@ class FormsolutionsITCase { private SendFormEndpoint endpoint; @MockitoBean - private VorgangManagerServerResolver resolver; - - @Mock - private Request request; - - @Mock - private VorgangServiceBlockingStub blockingStub; - @Mock - private ManagableStub<VorgangServiceBlockingStub> managableVorgangServiceBlockingStub; - @Mock - private BinaryFileServiceStub fileStub; - @Mock - private ManagableStub<BinaryFileServiceStub> managableBinaryFileStub; - @Mock - private CallStreamObserver<GrpcUploadBinaryFileRequest> fileStreamObserver; - - @BeforeEach - void initVorgangManagerResolver() { - when(resolver.resolveVorgangServiceBlockingStubByOrganisationseinheitenId(any())).thenReturn(managableVorgangServiceBlockingStub); - when(managableVorgangServiceBlockingStub.get()).thenReturn(blockingStub); - when(resolver.resolveBinaryFileServiceStubByOrganisationsEinheitId(any())).thenReturn(managableBinaryFileStub); - when(managableBinaryFileStub.get()).thenReturn(fileStub); - - Channel mockChannel = mock(Channel.class); - when(blockingStub.getChannel()).thenReturn(mockChannel); - when(blockingStub.startCreation(any())).thenReturn(GrpcCreateVorgangResponse.newBuilder().setVorgangId("42").build()); - - when(fileStub.uploadBinaryFileAsStream(any())).thenReturn(fileStreamObserver); - } + private VorgangService vorgangService; + + @Captor + private ArgumentCaptor<FormData> formDataCaptor; + @DisplayName("handle request") @Nested - class TestSendingVorgangBasicInformation { + class TestHandleRequest { - @Captor - private ArgumentCaptor<GrpcCreateVorgangRequest> createVorgangRequestCaptor; + private FormData capturedFormData; @BeforeEach void init() { - when(request.getJSON()).thenReturn(loadTextFile("SimpleJsonWithAttachments.json")); - } - - @Test - void shouldContainZustaendigeStelle() { - new Thread(() -> endpoint.receiveForm(request)).start(); - - var vorgangRequest = getCreateVorgangRequest(); + endpoint.handleRequest(createRequest()); - assertThat(vorgangRequest.getEingang().getZustaendigeStelle().getOrganisationseinheitenId()).isEqualTo("5678"); + verify(vorgangService).createVorgang(formDataCaptor.capture()); + capturedFormData = formDataCaptor.getValue(); } - private GrpcCreateVorgangRequest getCreateVorgangRequest() { - verify(blockingStub, timeout(1000)).startCreation(createVorgangRequestCaptor.capture()); - return createVorgangRequestCaptor.getValue(); + @DisplayName("should have zustaendige stelle") + @Test + void shouldHaveZustaendigeStelle() { + assertThat(capturedFormData.getZustaendigeStelles()) + .extracting(ZustaendigeStelle::getOrganisationseinheitenId) + .containsExactly("5678"); } - } - - @Nested - class TestReceiveFormWithAttachments { - - @Captor - private ArgumentCaptor<ClientResponseObserver<GrpcUploadBinaryFileRequest, GrpcUploadBinaryFileResponse>> observerCaptor; - - @Captor - private ArgumentCaptor<GrpcUploadBinaryFileRequest> requestCaptor; - @BeforeEach - void init() { - when(request.getJSON()).thenReturn(loadTextFile("SimpleJsonWithAttachments.json")); + @DisplayName("should have postfachId") + @Test + void shouldHavePostfachId() { + assertThat(capturedFormData.getHeader().getServiceKonto().getPostfachAddresses()) + .extracting(ServiceKonto.PostfachAddress::getIdentifier) + .extracting("postfachId") + .containsExactly("51522620-03d2-4507-b1f0-08d86920efed"); } + @DisplayName("should have requestId") @Test - void shouldSendContentOfAttachment() { - new Thread(() -> endpoint.receiveForm(request)).start(); - - var requests = getFileRequests(); - - var fileContent = requests.get(1).getFileContent(); - assertThat(fileContent.isEmpty()).isFalse(); + void shouldHaveVorgangsNummer() { + assertThat(capturedFormData.getHeader().getRequestId()) + .isEqualTo("KFAS_KOP_TEST-yCkgCdqG"); } + @DisplayName("should have representations") @Test - void shouldHaveContentType() { - new Thread(() -> endpoint.receiveForm(request)).start(); - - var requests = getFileRequests(); - - var contentType = requests.getFirst().getMetadata().getContentType(); - assertThat(contentType).isEqualTo("application/pdf"); + void shouldHaveRepresentations() { + assertThat(capturedFormData.getRepresentations()) + .extracting(IncomingFile::getName) + .containsExactlyInAnyOrder("form-data.json", "eingang.pdf"); } + @DisplayName("should have attachments") @Test - void shouldHaveFileSize() { - new Thread(() -> endpoint.receiveForm(request)).start(); - - var requests = getFileRequests(); - - var size = requests.getFirst().getMetadata().getSize(); - assertThat(size).isEqualTo(6788); + void shouldHaveAttachments() { + assertThat(capturedFormData.getAttachments()) + .flatExtracting(IncomingFileGroup::getFiles) + .extracting(IncomingFile::getName) + .containsExactly("aa2test.pdf"); } - private List<GrpcUploadBinaryFileRequest> getFileRequests() { - verify(fileStub, timeout(1000)).uploadBinaryFileAsStream(observerCaptor.capture()); - var onReadyHandler = (Runnable) ReflectionTestUtils.getField(observerCaptor.getValue(), "onReadyHandler"); - onReadyHandler.run(); - - verify(fileStreamObserver, new Timeout(2000, times(2))).onNext(requestCaptor.capture()); - var requests = requestCaptor.getAllValues(); - assertThat(requests).isNotEmpty(); - return requests; + @DisplayName("should have formData panels") + @Test + void shouldHaveFormDataPanels() { + assertThat(capturedFormData.getFormData()) + .containsAllEntriesOf(Map.of("Panel_0_1", Map.of( + "Textfeld (einzeilig)", "kfjhkfjhk", + "Datums- / Uhrzeitfeld", "22.05.1996" + ))); } - } - private static String loadTextFile(final String fileName) { - return TestUtils.loadTextFile(fileName); + private Request createRequest() { + var request = new Request(); + request.setJSON(TestUtils.loadTextFile("SimpleJsonWithAttachments.json")); + return request; } } diff --git a/src/test/resources/SimpleJsonWithAttachments.json b/src/test/resources/SimpleJsonWithAttachments.json index dc83b5be3e52d95c91b0edd576b36debf7126b58..a2652229efe3dfcd97383fa1831f69b9bd8ec0ef 100644 --- a/src/test/resources/SimpleJsonWithAttachments.json +++ b/src/test/resources/SimpleJsonWithAttachments.json @@ -17,5 +17,6 @@ "zustaendigeStelle" : "5678", "postkorbhandle" : "51522620-03d2-4507-b1f0-08d86920efed", "transactionId" : "KFAS_KOP_TEST-yCkgCdqG", + "pdf": "dGVzdCBwZGYgY29udGVudAo=", "zip": "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" } \ No newline at end of file