Skip to content
Snippets Groups Projects
Commit dcc5eaf9 authored by Jan Zickermann's avatar Jan Zickermann
Browse files

OZG-8070 Move processing to formsolutions-semantik

parent 6ef28da5
Branches
Tags
1 merge request!2Ozg 8070 map primary fom datar representation
Pipeline #2392 failed
...@@ -45,7 +45,7 @@ ...@@ -45,7 +45,7 @@
<properties> <properties>
<eingang-manager.version>2.20.0</eingang-manager.version> <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> <jaxb3-plugin.version>0.15.0</jaxb3-plugin.version>
<xmlschema.version>2.3.0</xmlschema.version> <xmlschema.version>2.3.0</xmlschema.version>
......
...@@ -24,9 +24,7 @@ ...@@ -24,9 +24,7 @@
package de.ozgcloud.eingang.formsolutions; package de.ozgcloud.eingang.formsolutions;
import java.io.File; import java.io.File;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import de.ozgcloud.common.binaryfile.FileDataDeserializer; import de.ozgcloud.common.binaryfile.FileDataDeserializer;
...@@ -37,17 +35,7 @@ import lombok.extern.jackson.Jacksonized; ...@@ -37,17 +35,7 @@ import lombok.extern.jackson.Jacksonized;
@Getter @Getter
@Builder @Builder
@Jacksonized @Jacksonized
public class FormSolutionsEingang { 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;
@JsonDeserialize(using = FileDataDeserializer.class) @JsonDeserialize(using = FileDataDeserializer.class)
private File pdf; private File pdf;
......
...@@ -26,8 +26,6 @@ package de.ozgcloud.eingang.formsolutions; ...@@ -26,8 +26,6 @@ package de.ozgcloud.eingang.formsolutions;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
...@@ -36,6 +34,7 @@ import org.springframework.http.MediaType; ...@@ -36,6 +34,7 @@ import org.springframework.http.MediaType;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import de.ozgcloud.common.errorhandling.TechnicalException; import de.ozgcloud.common.errorhandling.TechnicalException;
...@@ -52,13 +51,14 @@ class FormSolutionsRequestMapper { ...@@ -52,13 +51,14 @@ class FormSolutionsRequestMapper {
static final String FILE_NAME_JSON_REPRESENTATION = "form-data.json"; static final String FILE_NAME_JSON_REPRESENTATION = "form-data.json";
static final String FILE_NAME_PDF_REPRESENTATION = "eingang.pdf"; 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 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) { public FormData map(File jsonFile) {
var eingang = mapEingang(jsonFile); var eingang = mapEingang(jsonFile);
...@@ -68,7 +68,6 @@ class FormSolutionsRequestMapper { ...@@ -68,7 +68,6 @@ class FormSolutionsRequestMapper {
FormData buildFormData(File jsonFile, FormSolutionsEingang eingang) { FormData buildFormData(File jsonFile, FormSolutionsEingang eingang) {
var builder = FormData.builder() var builder = FormData.builder()
.formData(buildFormDataMap(eingang))
.attachments(attachmentMapper.mapAttachments(eingang.getZip())) .attachments(attachmentMapper.mapAttachments(eingang.getZip()))
.representation(buildJsonFile(jsonFile)); .representation(buildJsonFile(jsonFile));
var numberOfRepresentations = 1; var numberOfRepresentations = 1;
...@@ -97,23 +96,6 @@ class FormSolutionsRequestMapper { ...@@ -97,23 +96,6 @@ class FormSolutionsRequestMapper {
.build(); .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) { FormSolutionsEingang mapEingang(File jsonFile) {
try (var in = new FileInputStream(jsonFile)) { try (var in = new FileInputStream(jsonFile)) {
return objectMapper.readValue(in, FormSolutionsEingang.class); return objectMapper.readValue(in, FormSolutionsEingang.class);
......
...@@ -27,8 +27,6 @@ import java.io.File; ...@@ -27,8 +27,6 @@ import java.io.File;
import java.util.UUID; import java.util.UUID;
import java.util.function.Supplier; import java.util.function.Supplier;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.apache.logging.log4j.CloseableThreadContext; import org.apache.logging.log4j.CloseableThreadContext;
import org.springframework.ws.server.endpoint.annotation.Endpoint; import org.springframework.ws.server.endpoint.annotation.Endpoint;
...@@ -38,6 +36,8 @@ import org.springframework.ws.server.endpoint.annotation.ResponsePayload; ...@@ -38,6 +36,8 @@ import org.springframework.ws.server.endpoint.annotation.ResponsePayload;
import de.ozgcloud.common.binaryfile.TempFileUtils; import de.ozgcloud.common.binaryfile.TempFileUtils;
import de.ozgcloud.eingang.semantik.SemantikAdapter; import de.ozgcloud.eingang.semantik.SemantikAdapter;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
@Endpoint @Endpoint
@Log4j2 @Log4j2
...@@ -57,7 +57,7 @@ public class SendFormEndpoint { ...@@ -57,7 +57,7 @@ public class SendFormEndpoint {
return doSurroundOn(() -> handleRequest(request)); return doSurroundOn(() -> handleRequest(request));
} }
private Response handleRequest(Request request) { Response handleRequest(Request request) {
try { try {
semantikAdapter.processFormData(requestMapper.map(writeRequestJsonToFile(request.getJSON()))); semantikAdapter.processFormData(requestMapper.map(writeRequestJsonToFile(request.getJSON())));
return buildSuccessResponse(); return buildSuccessResponse();
......
/*
* 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);
}
}
/*
* 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;
}
}
...@@ -25,16 +25,12 @@ package de.ozgcloud.eingang.formsolutions; ...@@ -25,16 +25,12 @@ package de.ozgcloud.eingang.formsolutions;
import static de.ozgcloud.eingang.common.formdata.FormSolutionsTestFactory.*; import static de.ozgcloud.eingang.common.formdata.FormSolutionsTestFactory.*;
import static de.ozgcloud.eingang.formsolutions.FormSolutionsRequestMapper.*; 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.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*; import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import java.io.File; import java.io.File;
import java.io.InputStream;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
...@@ -48,23 +44,15 @@ import org.mockito.Mock; ...@@ -48,23 +44,15 @@ import org.mockito.Mock;
import org.mockito.Spy; import org.mockito.Spy;
import org.springframework.http.MediaType; 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.binaryfile.TempFileUtils;
import de.ozgcloud.common.errorhandling.TechnicalException; import de.ozgcloud.common.errorhandling.TechnicalException;
import de.ozgcloud.eingang.common.formdata.FormData; import de.ozgcloud.eingang.common.formdata.FormData;
import de.ozgcloud.eingang.common.formdata.IncomingFile; import de.ozgcloud.eingang.common.formdata.IncomingFile;
import de.ozgcloud.eingang.common.formdata.IncomingFileGroup; import de.ozgcloud.eingang.common.formdata.IncomingFileGroup;
import de.ozgcloud.eingang.common.formdata.IncomingFileGroupTestFactory; import de.ozgcloud.eingang.common.formdata.IncomingFileGroupTestFactory;
import lombok.SneakyThrows;
class FormSolutionsRequestMapperTest { class FormSolutionsRequestMapperTest {
private static final String COMPONENTS = "components";
private static final String STRING_VALUE = "stringValue";
private static final String PANELS = "panels";
@Spy @Spy
@InjectMocks @InjectMocks
private FormSolutionsRequestMapper mapper; private FormSolutionsRequestMapper mapper;
...@@ -72,15 +60,35 @@ class FormSolutionsRequestMapperTest { ...@@ -72,15 +60,35 @@ class FormSolutionsRequestMapperTest {
@Mock @Mock
private FormSolutionsAttachmentsMapper attachmentMapper; private FormSolutionsAttachmentsMapper attachmentMapper;
@Spy
private ObjectMapper objectMapper = new ObjectMapper();
private File simpleJsonFile; private File simpleJsonFile;
private File nestedComponenetJsonFile; private File nestedComponenetJsonFile;
@BeforeEach @BeforeEach
void writeJsonFile() { 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); nestedComponenetJsonFile = TempFileUtils.writeTmpFile(NESTED_COMPONENTS_JSON);
} }
...@@ -93,7 +101,8 @@ class FormSolutionsRequestMapperTest { ...@@ -93,7 +101,8 @@ class FormSolutionsRequestMapperTest {
@DisplayName("map") @DisplayName("map")
@Nested @Nested
class TestMap { class TestMap {
private final FormSolutionsEingang eingang = FormSolutionsEingangTestFactory.create(); @Mock
private FormSolutionsEingang eingang;
@BeforeEach @BeforeEach
void mock() { void mock() {
...@@ -145,28 +154,11 @@ class FormSolutionsRequestMapperTest { ...@@ -145,28 +154,11 @@ class FormSolutionsRequestMapperTest {
void mock() { void mock() {
when(eingang.getZip()).thenReturn(zipFile); when(eingang.getZip()).thenReturn(zipFile);
doReturn(Map.of(IDENTIFIER_KEY, STRING_VALUE)).when(mapper).buildFormDataMap(any());
when(attachmentMapper.mapAttachments(any())).thenReturn(List.of(attachmentGroup)); when(attachmentMapper.mapAttachments(any())).thenReturn(List.of(attachmentGroup));
doReturn(jsonIncomingFile).when(mapper).buildJsonFile(any()); doReturn(jsonIncomingFile).when(mapper).buildJsonFile(any());
doReturn(formDataControl).when(mapper).buildFormDataControl(); 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") @DisplayName("should call mapAttachments")
@Test @Test
void shouldCallMapAttachments() { void shouldCallMapAttachments() {
...@@ -356,113 +348,37 @@ class FormSolutionsRequestMapperTest { ...@@ -356,113 +348,37 @@ class FormSolutionsRequestMapperTest {
} }
@DisplayName("map eingang")
@Nested @Nested
class TestJsonToEingangMapping { class TestMapEingang {
@Test
void shouldMapControlValues() {
var eingang = mapper.mapEingang(simpleJsonFile);
assertThat(eingang).isNotNull().usingRecursiveComparison()
.ignoringFields("zip", "pdf", "assistant").isEqualTo(FormSolutionsEingangTestFactory.create());
}
@Test
void shouldHaveAssistantData() {
var eingang = mapper.mapEingang(simpleJsonFile);
assertThat(eingang.getAssistant()).isNotEmpty();
}
@Test
@SneakyThrows
void shouldHandleJsonException() throws JsonProcessingException {
doThrow(JsonProcessingException.class).when(objectMapper).readValue(any(InputStream.class), eq(FormSolutionsEingang.class));
assertThatThrownBy(() -> mapper.mapEingang(simpleJsonFile)).isInstanceOf(TechnicalException.class);
}
@DisplayName("should return zip")
@Test @Test
void shouldContainFormIdentifier() { void shouldReturn() {
var eingang = mapper.mapEingang(simpleJsonFile); var result = mapEingang();
assertThat(eingang.getAssistant()).containsEntry(IDENTIFIER_KEY, IDENTIFIER_VALUE); assertThat(result.getZip().length()).isGreaterThan(0);
} }
@Nested @DisplayName("should return pdf")
class TestPanels {
@Test @Test
void shouldContainPanels() { void shouldReturnPdf() {
var eingang = mapper.mapEingang(simpleJsonFile); var result = mapEingang();
assertThat(getPanels(eingang)).isNotNull(); assertThat(result.getPdf().length()).isGreaterThan(0);
} }
@DisplayName("should throw with bad json")
@Test @Test
void shouldContainPanelIdentifier() { void shouldThrowWithBadJson() {
var eingang = mapper.mapEingang(simpleJsonFile); var badJsonFile = TempFileUtils.writeTmpFile("{ \"zustaendigeStell...");
assertThat(getPanels(eingang).getFirst()).containsEntry(IDENTIFIER_KEY, PANEL_ID); assertThatThrownBy(() -> mapper.mapEingang(badJsonFile))
.isInstanceOf(TechnicalException.class);
} }
@Test FormSolutionsEingang mapEingang() {
void shouldContainPanelComponets() { return mapper.mapEingang(simpleJsonFile);
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());
} }
} }
...@@ -529,29 +445,4 @@ class FormSolutionsRequestMapperTest { ...@@ -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);
}
}
} }
...@@ -23,38 +23,30 @@ ...@@ -23,38 +23,30 @@
*/ */
package de.ozgcloud.eingang.formsolutions; package de.ozgcloud.eingang.formsolutions;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.*; 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.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.Captor; import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.verification.Timeout;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.util.ReflectionTestUtils;
import de.ozgcloud.common.test.TestUtils; import de.ozgcloud.common.test.TestUtils;
import de.ozgcloud.eingang.router.ManagableStub; import de.ozgcloud.eingang.common.formdata.FormData;
import de.ozgcloud.eingang.router.VorgangManagerServerResolver; import de.ozgcloud.eingang.common.formdata.IncomingFile;
import de.ozgcloud.vorgang.grpc.binaryFile.BinaryFileServiceGrpc.BinaryFileServiceStub; import de.ozgcloud.eingang.common.formdata.IncomingFileGroup;
import de.ozgcloud.vorgang.grpc.binaryFile.GrpcUploadBinaryFileRequest; import de.ozgcloud.eingang.common.formdata.ServiceKonto;
import de.ozgcloud.vorgang.grpc.binaryFile.GrpcUploadBinaryFileResponse; import de.ozgcloud.eingang.common.formdata.ZustaendigeStelle;
import de.ozgcloud.vorgang.vorgang.GrpcCreateVorgangRequest; import de.ozgcloud.eingang.router.VorgangService;
import de.ozgcloud.vorgang.vorgang.GrpcCreateVorgangResponse;
import de.ozgcloud.vorgang.vorgang.VorgangServiceGrpc.VorgangServiceBlockingStub;
@SpringBootTest @SpringBootTest
@DirtiesContext @DirtiesContext
...@@ -65,121 +57,81 @@ class FormsolutionsITCase { ...@@ -65,121 +57,81 @@ class FormsolutionsITCase {
private SendFormEndpoint endpoint; private SendFormEndpoint endpoint;
@MockitoBean @MockitoBean
private VorgangManagerServerResolver resolver; private VorgangService vorgangService;
@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 @Captor
void initVorgangManagerResolver() { private ArgumentCaptor<FormData> formDataCaptor;
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);
}
@DisplayName("handle request")
@Nested @Nested
class TestSendingVorgangBasicInformation { class TestHandleRequest {
@Captor private FormData capturedFormData;
private ArgumentCaptor<GrpcCreateVorgangRequest> createVorgangRequestCaptor;
@BeforeEach @BeforeEach
void init() { void init() {
when(request.getJSON()).thenReturn(loadTextFile("SimpleJsonWithAttachments.json")); endpoint.handleRequest(createRequest());
}
@Test verify(vorgangService).createVorgang(formDataCaptor.capture());
void shouldContainZustaendigeStelle() { capturedFormData = formDataCaptor.getValue();
new Thread(() -> endpoint.receiveForm(request)).start();
var vorgangRequest = getCreateVorgangRequest();
assertThat(vorgangRequest.getEingang().getZustaendigeStelle().getOrganisationseinheitenId()).isEqualTo("5678");
} }
private GrpcCreateVorgangRequest getCreateVorgangRequest() { @DisplayName("should have zustaendige stelle")
verify(blockingStub, timeout(1000)).startCreation(createVorgangRequestCaptor.capture()); @Test
return createVorgangRequestCaptor.getValue(); void shouldHaveZustaendigeStelle() {
} assertThat(capturedFormData.getZustaendigeStelles())
.extracting(ZustaendigeStelle::getOrganisationseinheitenId)
.containsExactly("5678");
} }
@Nested @DisplayName("should have postfachId")
class TestReceiveFormWithAttachments { @Test
void shouldHavePostfachId() {
@Captor assertThat(capturedFormData.getHeader().getServiceKonto().getPostfachAddresses())
private ArgumentCaptor<ClientResponseObserver<GrpcUploadBinaryFileRequest, GrpcUploadBinaryFileResponse>> observerCaptor; .extracting(ServiceKonto.PostfachAddress::getIdentifier)
.extracting("postfachId")
@Captor .containsExactly("51522620-03d2-4507-b1f0-08d86920efed");
private ArgumentCaptor<GrpcUploadBinaryFileRequest> requestCaptor;
@BeforeEach
void init() {
when(request.getJSON()).thenReturn(loadTextFile("SimpleJsonWithAttachments.json"));
} }
@DisplayName("should have requestId")
@Test @Test
void shouldSendContentOfAttachment() { void shouldHaveVorgangsNummer() {
new Thread(() -> endpoint.receiveForm(request)).start(); assertThat(capturedFormData.getHeader().getRequestId())
.isEqualTo("KFAS_KOP_TEST-yCkgCdqG");
var requests = getFileRequests();
var fileContent = requests.get(1).getFileContent();
assertThat(fileContent.isEmpty()).isFalse();
} }
@DisplayName("should have representations")
@Test @Test
void shouldHaveContentType() { void shouldHaveRepresentations() {
new Thread(() -> endpoint.receiveForm(request)).start(); assertThat(capturedFormData.getRepresentations())
.extracting(IncomingFile::getName)
var requests = getFileRequests(); .containsExactlyInAnyOrder("form-data.json", "eingang.pdf");
var contentType = requests.getFirst().getMetadata().getContentType();
assertThat(contentType).isEqualTo("application/pdf");
} }
@DisplayName("should have attachments")
@Test @Test
void shouldHaveFileSize() { void shouldHaveAttachments() {
new Thread(() -> endpoint.receiveForm(request)).start(); assertThat(capturedFormData.getAttachments())
.flatExtracting(IncomingFileGroup::getFiles)
var requests = getFileRequests(); .extracting(IncomingFile::getName)
.containsExactly("aa2test.pdf");
var size = requests.getFirst().getMetadata().getSize();
assertThat(size).isEqualTo(6788);
} }
private List<GrpcUploadBinaryFileRequest> getFileRequests() { @DisplayName("should have formData panels")
verify(fileStub, timeout(1000)).uploadBinaryFileAsStream(observerCaptor.capture()); @Test
var onReadyHandler = (Runnable) ReflectionTestUtils.getField(observerCaptor.getValue(), "onReadyHandler"); void shouldHaveFormDataPanels() {
onReadyHandler.run(); assertThat(capturedFormData.getFormData())
.containsAllEntriesOf(Map.of("Panel_0_1", Map.of(
verify(fileStreamObserver, new Timeout(2000, times(2))).onNext(requestCaptor.capture()); "Textfeld (einzeilig)", "kfjhkfjhk",
var requests = requestCaptor.getAllValues(); "Datums- / Uhrzeitfeld", "22.05.1996"
assertThat(requests).isNotEmpty(); )));
return requests;
} }
} }
private static String loadTextFile(final String fileName) { private Request createRequest() {
return TestUtils.loadTextFile(fileName); var request = new Request();
request.setJSON(TestUtils.loadTextFile("SimpleJsonWithAttachments.json"));
return request;
} }
} }
...@@ -17,5 +17,6 @@ ...@@ -17,5 +17,6 @@
"zustaendigeStelle" : "5678", "zustaendigeStelle" : "5678",
"postkorbhandle" : "51522620-03d2-4507-b1f0-08d86920efed", "postkorbhandle" : "51522620-03d2-4507-b1f0-08d86920efed",
"transactionId" : "KFAS_KOP_TEST-yCkgCdqG", "transactionId" : "KFAS_KOP_TEST-yCkgCdqG",
"pdf": "dGVzdCBwZGYgY29udGVudAo=",
"zip": "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" "zip": "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"
} }
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment