From dfb99db8f231866805378895c7a04a0a06ec6e2c Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:11:52 +0200 Subject: [PATCH 01/25] =?UTF-8?q?=F0=9F=9A=A7=20draft=20of=20get=20post=20?= =?UTF-8?q?handbuch=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../basisdatenservice/domain/Handbuch.java | 28 ++++++++ .../domain/HandbuchRepository.java | 46 +++++++++++++ .../domain/WahlbezirkArt.java | 5 ++ .../domain/WahltagIdUndWahlbezirksart.java | 19 ++++++ .../exception/ExceptionConstants.java | 7 ++ .../rest/handbuch/HandbuchController.java | 66 +++++++++++++++++++ .../rest/handbuch/WahlbezirkArtDTO.java | 5 ++ .../handbuch/HandbuchModelMapper.java | 17 +++++ .../handbuch/HandbuchReferenceModel.java | 7 ++ .../services/handbuch/HandbuchService.java | 49 ++++++++++++++ .../services/handbuch/HandbuchValidator.java | 27 ++++++++ .../services/handbuch/HandbuchWriteModel.java | 7 ++ .../services/handbuch/WahlbezirkArtModel.java | 5 ++ 13 files changed, 288 insertions(+) create mode 100644 wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/Handbuch.java create mode 100644 wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/HandbuchRepository.java create mode 100644 wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/WahlbezirkArt.java create mode 100644 wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/WahltagIdUndWahlbezirksart.java create mode 100644 wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java create mode 100644 wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/WahlbezirkArtDTO.java create mode 100644 wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchModelMapper.java create mode 100644 wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchReferenceModel.java create mode 100644 wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchService.java create mode 100644 wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidator.java create mode 100644 wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchWriteModel.java create mode 100644 wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/WahlbezirkArtModel.java diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/Handbuch.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/Handbuch.java new file mode 100644 index 000000000..f506dbad1 --- /dev/null +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/Handbuch.java @@ -0,0 +1,28 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.domain; + +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Lob; +import jakarta.validation.constraints.NotNull; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@EqualsAndHashCode +@ToString(onlyExplicitlyIncluded = true) +public class Handbuch { + + @EmbeddedId + @ToString.Include + private WahltagIdUndWahlbezirksart wahltagIdUndWahlbezirksart; + + @NotNull + @Lob + private byte[] handbuch; +} diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/HandbuchRepository.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/HandbuchRepository.java new file mode 100644 index 000000000..76008bfd2 --- /dev/null +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/HandbuchRepository.java @@ -0,0 +1,46 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.domain; + +import java.util.Optional; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.repository.CrudRepository; +import org.springframework.security.access.prepost.PreAuthorize; + +@PreAuthorize("hasAuthority('Basisdaten_READ_Handbuch')") +public interface HandbuchRepository extends CrudRepository { + + String CACHE = "HANDBUCH_CACHE"; + + @Override + Iterable findAll(); + + @Override + @Cacheable(value = CACHE, key = "#p0") + Optional findById(WahltagIdUndWahlbezirksart wahltagIdUndWahlbezirksart); + + @Override + @CachePut(value = CACHE, key = "#p0.wahltagIdUndWahlbezirksart") + @PreAuthorize("hasAuthority('Basisdaten_WRITE_Handbuch')") + S save(S handbuch); + + @Override + @CacheEvict(value = CACHE, key = "#p0") + @PreAuthorize("hasAuthority('Basisdaten_DELETE_Handbuch')") + void deleteById(WahltagIdUndWahlbezirksart wahltagIdUndWahlbezirksart); + + @Override + @CacheEvict(value = CACHE, key = "#p0.wahltagIdUndWahlbezirksart") + @PreAuthorize("hasAuthority('Basisdaten_DELETE_Handbuch')") + void delete(Handbuch entity); + + @Override + @CacheEvict(value = CACHE, allEntries = true) + @PreAuthorize("hasAuthority('Basisdaten_DELETE_Handbuch')") + void deleteAll(Iterable entities); + + @Override + @CacheEvict(value = CACHE, allEntries = true) + @PreAuthorize("hasAuthority('Basisdaten_DELETE_Handbuch')") + void deleteAll(); +} diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/WahlbezirkArt.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/WahlbezirkArt.java new file mode 100644 index 000000000..04f245bd4 --- /dev/null +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/WahlbezirkArt.java @@ -0,0 +1,5 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.domain; + +public enum WahlbezirkArt { + UWB, BWB +} diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/WahltagIdUndWahlbezirksart.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/WahltagIdUndWahlbezirksart.java new file mode 100644 index 000000000..88aa4fec4 --- /dev/null +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/WahltagIdUndWahlbezirksart.java @@ -0,0 +1,19 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.domain; + +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; + +@Data +public class WahltagIdUndWahlbezirksart { + + @NotNull + @Size(max = 1024) + private String wahltagID; + + @Enumerated(EnumType.STRING) + @NotNull + private WahlbezirkArt wahlbezirksart; +} diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/exception/ExceptionConstants.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/exception/ExceptionConstants.java index 17477c786..865be3813 100644 --- a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/exception/ExceptionConstants.java +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/exception/ExceptionConstants.java @@ -24,4 +24,11 @@ public class ExceptionConstants { public static ExceptionDataWrapper FAILED_COMMUNICATION_WITH_EAI = new ExceptionDataWrapper("100", "Bei der Kommunikation mit dem Aoueai-Service ist ein Fehler aufgetreten. Es konnten daher keine Daten geladen werden."); + public static ExceptionDataWrapper GETHANDBUCH_PARAMETER_UNVOLLSTAENDIG = new ExceptionDataWrapper("301", "getHandbuch: Suchkriterien unvollständig."); + public static ExceptionDataWrapper GETHANDBUCH_KEINE_DATEN = new ExceptionDataWrapper("302", "Das Handbuch konnte nicht geladen werden."); + + public static ExceptionDataWrapper POSTHANDBUCH_PARAMETER_UNVOLLSTAENDIG = new ExceptionDataWrapper("315", "postHandbuch: Suchkriterien unvollständig."); + public static ExceptionDataWrapper POSTHANDBUCH_SPEICHERN_NICHT_ERFOLGREICH = new ExceptionDataWrapper("316", + "postHandbuch: Das speichern des Handbuches war nicht erfolgreich."); + } diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java new file mode 100644 index 000000000..ea9b77a4e --- /dev/null +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java @@ -0,0 +1,66 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.rest.handbuch; + +import de.muenchen.oss.wahllokalsystem.basisdatenservice.exception.ExceptionConstants; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchReferenceModel; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchService; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchWriteModel; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.WahlbezirkArtModel; +import de.muenchen.oss.wahllokalsystem.wls.common.exception.util.ExceptionFactory; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartHttpServletRequest; + +@RestController +@RequestMapping("/businessActions/handbuch") +@RequiredArgsConstructor +@Slf4j +public class HandbuchController { + + private final HandbuchService handbuchService; + + private final ExceptionFactory exceptionFactory; + + @GetMapping("{wahltagID}/{wahlbezirksart}") + public void getHandbuch(@PathVariable("wahltagID") String wahltagID, @PathVariable("wahlbezirksart") WahlbezirkArtDTO wahlbezirkArtDTO, + HttpServletResponse response) { + + val handbuchData = handbuchService.getHandbuch( + new HandbuchReferenceModel(wahltagID, WahlbezirkArtModel.valueOf(wahlbezirkArtDTO.name()))); //TODO Mapper verwenden + + try { + response.setContentType("application/pdf"); + response.setHeader("Content-Disposition", "attachment; filename=" + wahlbezirkArtDTO + "Handbuch.pdf"); + response.getOutputStream().write(handbuchData); + response.flushBuffer(); + } catch (IOException e) { + log.error("#getHandbuch: Bei der Verarbeitung des Handbuches ist ein Fehler aufgetreten: {}", e); + try { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + } catch (IOException e1) { + log.error("#getHandbuch: IOException by sendError {}", e1); + } + } + } + + @PostMapping("{wahltagID}/{wahlbezirksart}") + public void getHandbuch(@PathVariable("wahltagID") String wahltagID, @PathVariable("wahlbezirksart") WahlbezirkArtDTO wahlbezirkArtDTO, + MultipartHttpServletRequest request) { + val itr = request.getFileNames(); + val file = request.getFile(itr.next()); + + try { + handbuchService.setHandbuch(new HandbuchWriteModel(new HandbuchReferenceModel(wahltagID, WahlbezirkArtModel.valueOf(wahlbezirkArtDTO.name())), + file.getBytes())); + } catch (final IOException e) { + throw exceptionFactory.createTechnischeWlsException(ExceptionConstants.POSTHANDBUCH_SPEICHERN_NICHT_ERFOLGREICH); + } + } +} diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/WahlbezirkArtDTO.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/WahlbezirkArtDTO.java new file mode 100644 index 000000000..223f487f1 --- /dev/null +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/WahlbezirkArtDTO.java @@ -0,0 +1,5 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.rest.handbuch; + +public enum WahlbezirkArtDTO { + UWB, BWB +} diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchModelMapper.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchModelMapper.java new file mode 100644 index 000000000..3c1251e51 --- /dev/null +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchModelMapper.java @@ -0,0 +1,17 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch; + +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.Handbuch; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.WahltagIdUndWahlbezirksart; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +@Mapper +public interface HandbuchModelMapper { + + WahltagIdUndWahlbezirksart toEntityID(HandbuchReferenceModel handbuchReferenceModel); + + @Mapping(target = "wahltagIdUndWahlbezirksart.wahltagID", source = "handbuchReferenceModel.wahltagID") + @Mapping(target = "wahltagIdUndWahlbezirksart.wahlbezirksart", source = "handbuchReferenceModel.wahlbezirksart") + @Mapping(target = "handbuch", source = "handbuchData") + Handbuch toEntity(HandbuchWriteModel handbuchWriteModel); +} diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchReferenceModel.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchReferenceModel.java new file mode 100644 index 000000000..f113786f0 --- /dev/null +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchReferenceModel.java @@ -0,0 +1,7 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch; + +import jakarta.validation.constraints.NotNull; + +public record HandbuchReferenceModel(@NotNull String wahltagID, + @NotNull WahlbezirkArtModel wahlbezirksart) { +} diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchService.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchService.java new file mode 100644 index 000000000..4ae4a7612 --- /dev/null +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchService.java @@ -0,0 +1,49 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch; + +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.Handbuch; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.HandbuchRepository; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.WahltagIdUndWahlbezirksart; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.exception.ExceptionConstants; +import de.muenchen.oss.wahllokalsystem.wls.common.exception.util.ExceptionFactory; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import lombok.val; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +@Slf4j +public class HandbuchService { + + private final HandbuchValidator handbuchValidator; + + private final HandbuchModelMapper handbuchModelMapper; + + private final HandbuchRepository handbuchRepository; + + private final ExceptionFactory exceptionFactory; + + public byte[] getHandbuch(final HandbuchReferenceModel handbuchReference) { + log.info("#getHandbuch > reference > {}", handbuchReference); + + handbuchValidator.validHandbuchReferenceOrThrow(handbuchReference); + val handbuchID = handbuchModelMapper.toEntityID(handbuchReference); + + return findByIDOrThrowNoData(handbuchID).getHandbuch(); + } + + public void setHandbuch(final HandbuchWriteModel handbuchWriteModel) { + handbuchValidator.validHandbuchWriteModelOrThrow(handbuchWriteModel); + val entityToSave = handbuchModelMapper.toEntity(handbuchWriteModel); + try { + handbuchRepository.save(entityToSave); + } catch (final Exception e) { + throw exceptionFactory.createTechnischeWlsException(ExceptionConstants.POSTHANDBUCH_SPEICHERN_NICHT_ERFOLGREICH); + } + } + + private Handbuch findByIDOrThrowNoData(final WahltagIdUndWahlbezirksart handbuchReference) { + return handbuchRepository.findById(handbuchReference) + .orElseThrow(() -> exceptionFactory.createTechnischeWlsException(ExceptionConstants.GETHANDBUCH_KEINE_DATEN)); + } +} diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidator.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidator.java new file mode 100644 index 000000000..10e6024b4 --- /dev/null +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidator.java @@ -0,0 +1,27 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch; + +import de.muenchen.oss.wahllokalsystem.basisdatenservice.exception.ExceptionConstants; +import de.muenchen.oss.wahllokalsystem.wls.common.exception.util.ExceptionFactory; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class HandbuchValidator { + + private final ExceptionFactory exceptionFactory; + + public void validHandbuchReferenceOrThrow(final HandbuchReferenceModel handbuchReference) { + if (handbuchReference == null || StringUtils.isBlank(handbuchReference.wahltagID()) || handbuchReference.wahlbezirksart() == null) { + throw exceptionFactory.createFachlicheWlsException(ExceptionConstants.GETHANDBUCH_PARAMETER_UNVOLLSTAENDIG); + } + } + + public void validHandbuchWriteModelOrThrow(final HandbuchWriteModel handbuchWriteModel) { + if (handbuchWriteModel == null || handbuchWriteModel.handbuchReferenceModel() == null || StringUtils.isBlank( + handbuchWriteModel.handbuchReferenceModel().wahltagID()) || handbuchWriteModel.handbuchReferenceModel().wahlbezirksart() == null) { + throw exceptionFactory.createFachlicheWlsException(ExceptionConstants.POSTHANDBUCH_PARAMETER_UNVOLLSTAENDIG); + } + } +} diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchWriteModel.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchWriteModel.java new file mode 100644 index 000000000..ff0da690e --- /dev/null +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchWriteModel.java @@ -0,0 +1,7 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch; + +import jakarta.validation.constraints.NotNull; + +public record HandbuchWriteModel(@NotNull HandbuchReferenceModel handbuchReferenceModel, + @NotNull byte[] handbuchData) { +} diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/WahlbezirkArtModel.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/WahlbezirkArtModel.java new file mode 100644 index 000000000..524e22717 --- /dev/null +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/WahlbezirkArtModel.java @@ -0,0 +1,5 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch; + +public enum WahlbezirkArtModel { + UWB, BWB +} From 4debc0b097b2b2ccd32ea53aba6c06a214e05aa4 Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Tue, 9 Jul 2024 18:24:34 +0200 Subject: [PATCH 02/25] add flyway scripts for handbuch --- .../db/migrations/h2/V2_0__createHandbuchTable.sql | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 wls-basisdaten-service/src/main/resources/db/migrations/h2/V2_0__createHandbuchTable.sql diff --git a/wls-basisdaten-service/src/main/resources/db/migrations/h2/V2_0__createHandbuchTable.sql b/wls-basisdaten-service/src/main/resources/db/migrations/h2/V2_0__createHandbuchTable.sql new file mode 100644 index 000000000..7eebfc8bf --- /dev/null +++ b/wls-basisdaten-service/src/main/resources/db/migrations/h2/V2_0__createHandbuchTable.sql @@ -0,0 +1,8 @@ +CREATE TABLE Handbuch +( + wahltagid varchar(1024) not null, + wahlbezirksart varchar(255) not null, + handbuch blob not null, + + primary key (wahltagid, wahlbezirksart) +); From f0a7275a6550b803aa03a72814859db7943d2006 Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Tue, 9 Jul 2024 18:25:04 +0200 Subject: [PATCH 03/25] finalize implementation of get and post handbuch --- .../rest/handbuch/HandbuchController.java | 66 +++++++++++-------- .../rest/handbuch/HandbuchDTOMapper.java | 13 ++++ .../services/handbuch/HandbuchService.java | 7 +- 3 files changed, 55 insertions(+), 31 deletions(-) create mode 100644 wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchDTOMapper.java diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java index ea9b77a4e..8365345b3 100644 --- a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java @@ -1,16 +1,16 @@ package de.muenchen.oss.wahllokalsystem.basisdatenservice.rest.handbuch; import de.muenchen.oss.wahllokalsystem.basisdatenservice.exception.ExceptionConstants; -import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchReferenceModel; import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchService; -import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchWriteModel; -import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.WahlbezirkArtModel; import de.muenchen.oss.wahllokalsystem.wls.common.exception.util.ExceptionFactory; -import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import lombok.val; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -24,43 +24,51 @@ @Slf4j public class HandbuchController { + @Value("${service.config.manual.filenamesuffix:Handbuch.pdf}") + String manualFileNameSuffix; + private final HandbuchService handbuchService; private final ExceptionFactory exceptionFactory; - @GetMapping("{wahltagID}/{wahlbezirksart}") - public void getHandbuch(@PathVariable("wahltagID") String wahltagID, @PathVariable("wahlbezirksart") WahlbezirkArtDTO wahlbezirkArtDTO, - HttpServletResponse response) { - - val handbuchData = handbuchService.getHandbuch( - new HandbuchReferenceModel(wahltagID, WahlbezirkArtModel.valueOf(wahlbezirkArtDTO.name()))); //TODO Mapper verwenden + private final HandbuchDTOMapper handbuchDTOMapper; - try { - response.setContentType("application/pdf"); - response.setHeader("Content-Disposition", "attachment; filename=" + wahlbezirkArtDTO + "Handbuch.pdf"); - response.getOutputStream().write(handbuchData); - response.flushBuffer(); - } catch (IOException e) { - log.error("#getHandbuch: Bei der Verarbeitung des Handbuches ist ein Fehler aufgetreten: {}", e); - try { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - } catch (IOException e1) { - log.error("#getHandbuch: IOException by sendError {}", e1); - } - } + @GetMapping("{wahltagID}/{wahlbezirksart}") + public ResponseEntity getHandbuch(@PathVariable("wahltagID") String wahltagID, + @PathVariable("wahlbezirksart") WahlbezirkArtDTO wahlbezirkArtDTO) { + val handbuchData = handbuchService.getHandbuch(handbuchDTOMapper.toModel(wahltagID, wahlbezirkArtDTO)); + return createPDFResponse(handbuchData, wahlbezirkArtDTO); } @PostMapping("{wahltagID}/{wahlbezirksart}") public void getHandbuch(@PathVariable("wahltagID") String wahltagID, @PathVariable("wahlbezirksart") WahlbezirkArtDTO wahlbezirkArtDTO, - MultipartHttpServletRequest request) { - val itr = request.getFileNames(); - val file = request.getFile(itr.next()); - + final MultipartHttpServletRequest request) { try { - handbuchService.setHandbuch(new HandbuchWriteModel(new HandbuchReferenceModel(wahltagID, WahlbezirkArtModel.valueOf(wahlbezirkArtDTO.name())), - file.getBytes())); + val handbuchData = getHandbuchFromRequest(request); + val modelToSet = handbuchDTOMapper.toModel(handbuchDTOMapper.toModel(wahltagID, wahlbezirkArtDTO), handbuchData); + handbuchService.setHandbuch(modelToSet); } catch (final IOException e) { throw exceptionFactory.createTechnischeWlsException(ExceptionConstants.POSTHANDBUCH_SPEICHERN_NICHT_ERFOLGREICH); } } + + private byte[] getHandbuchFromRequest(final MultipartHttpServletRequest request) throws IOException { + val fileName = request.getFileNames().next(); + log.debug("using filename > {}", fileName); + val file = request.getFile(fileName); + + if (file == null) { + throw new IOException("No file was uploaded"); + } + + return file.getBytes(); + } + + private ResponseEntity createPDFResponse(final byte[] responseBody, final WahlbezirkArtDTO wahlbezirkArtDTO) { + val responseHeaders = new HttpHeaders(); + responseHeaders.add(HttpHeaders.CONTENT_TYPE, "application/pdf"); + responseHeaders.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + wahlbezirkArtDTO + manualFileNameSuffix); + + return new ResponseEntity<>(responseBody, responseHeaders, HttpStatus.OK); + } } diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchDTOMapper.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchDTOMapper.java new file mode 100644 index 000000000..524ea04d6 --- /dev/null +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchDTOMapper.java @@ -0,0 +1,13 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.rest.handbuch; + +import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchReferenceModel; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchWriteModel; +import org.mapstruct.Mapper; + +@Mapper +public interface HandbuchDTOMapper { + + HandbuchWriteModel toModel(HandbuchReferenceModel handbuchReferenceModel, byte[] handbuchData); + + HandbuchReferenceModel toModel(String wahltagID, WahlbezirkArtDTO wahlbezirksart); +} diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchService.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchService.java index 4ae4a7612..f34626a1b 100644 --- a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchService.java +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchService.java @@ -5,6 +5,7 @@ import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.WahltagIdUndWahlbezirksart; import de.muenchen.oss.wahllokalsystem.basisdatenservice.exception.ExceptionConstants; import de.muenchen.oss.wahllokalsystem.wls.common.exception.util.ExceptionFactory; +import java.util.Arrays; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import lombok.val; @@ -24,15 +25,17 @@ public class HandbuchService { private final ExceptionFactory exceptionFactory; public byte[] getHandbuch(final HandbuchReferenceModel handbuchReference) { - log.info("#getHandbuch > reference > {}", handbuchReference); + log.info("#getHandbuch - handbuchReference > {}", handbuchReference); handbuchValidator.validHandbuchReferenceOrThrow(handbuchReference); val handbuchID = handbuchModelMapper.toEntityID(handbuchReference); - return findByIDOrThrowNoData(handbuchID).getHandbuch(); + val handbuchData = findByIDOrThrowNoData(handbuchID).getHandbuch(); + return Arrays.copyOf(handbuchData, handbuchData.length); } public void setHandbuch(final HandbuchWriteModel handbuchWriteModel) { + log.info("postHandbuch - handbuchWriteModel> {}", handbuchWriteModel); handbuchValidator.validHandbuchWriteModelOrThrow(handbuchWriteModel); val entityToSave = handbuchModelMapper.toEntity(handbuchWriteModel); try { From 2b831e7a948da78fd38337dae31076ec6fb3eb20 Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Tue, 9 Jul 2024 18:25:28 +0200 Subject: [PATCH 04/25] add example pdf file for upload request --- .../test/resources/attachements/helloWorld.pdf | Bin 0 -> 47873 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 wls-basisdaten-service/src/test/resources/attachements/helloWorld.pdf diff --git a/wls-basisdaten-service/src/test/resources/attachements/helloWorld.pdf b/wls-basisdaten-service/src/test/resources/attachements/helloWorld.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b3cc90be756073bd7f2a8a73b481040b34e35a4b GIT binary patch literal 47873 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 Date: Tue, 9 Jul 2024 18:27:45 +0200 Subject: [PATCH 05/25] create example http requests for handbuch --- .../src/test/resources/handbuch.http | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 wls-basisdaten-service/src/test/resources/handbuch.http diff --git a/wls-basisdaten-service/src/test/resources/handbuch.http b/wls-basisdaten-service/src/test/resources/handbuch.http new file mode 100644 index 000000000..7ba32194c --- /dev/null +++ b/wls-basisdaten-service/src/test/resources/handbuch.http @@ -0,0 +1,33 @@ +### Get token wls_all +POST {{ SSO_URL }}/auth/realms/wls_realm/protocol/openid-connect/token +Content-Type: application/x-www-form-urlencoded + +password = test & +grant_type = password & +client_secret = top-secret & +client_id = wls & +username = wls_all + +> {% + client.global.set("auth_token", response.body.access_token); + client.global.set("token_type", response.body.token_type); +%} + +### get userinfo with auth_token +GET {{ SSO_URL }}/auth/realms/wls_realm/protocol/openid-connect/userinfo +Authorization: {{ token_type }} {{ auth_token }} + +### POST Handbuch +POST {{ WLS_BASISDATEN_SERVICE_URL }}/businessActions/handbuch/wahlID1/UWB +Authorization: {{ token_type }} {{ auth_token }} +Content-Type: multipart/form-data; boundary=boundary + +--boundary +Content-Disposition: form-data; name="file"; filename="helloWorld.pdf" + +< ./attachements/helloWorld.pdf +--boundary + +### GET Handbuch +GET {{ WLS_BASISDATEN_SERVICE_URL }}/businessActions/handbuch/wahlID1/UWB +Authorization: {{ token_type }} {{ auth_token }} \ No newline at end of file From 992bc8085fc7840e625ddcdb40d308f3c0af02f2 Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Tue, 9 Jul 2024 18:32:58 +0200 Subject: [PATCH 06/25] add PreAuthorize to Service --- .../basisdatenservice/services/handbuch/HandbuchService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchService.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchService.java index f34626a1b..b3eefcb8d 100644 --- a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchService.java +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchService.java @@ -9,6 +9,7 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import lombok.val; +import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.stereotype.Service; @Service @@ -24,6 +25,7 @@ public class HandbuchService { private final ExceptionFactory exceptionFactory; + @PreAuthorize("hasAuthority('Basisdaten_BUSINESSACTION_GetHandbuch')") public byte[] getHandbuch(final HandbuchReferenceModel handbuchReference) { log.info("#getHandbuch - handbuchReference > {}", handbuchReference); @@ -34,6 +36,7 @@ public byte[] getHandbuch(final HandbuchReferenceModel handbuchReference) { return Arrays.copyOf(handbuchData, handbuchData.length); } + @PreAuthorize("hasAuthority('Basisdaten_BUSINESSACTION_PostHandbuch')") public void setHandbuch(final HandbuchWriteModel handbuchWriteModel) { log.info("postHandbuch - handbuchWriteModel> {}", handbuchWriteModel); handbuchValidator.validHandbuchWriteModelOrThrow(handbuchWriteModel); From 340955d07d032120358897b07ca2f9269f7e8f3a Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Wed, 10 Jul 2024 13:39:55 +0200 Subject: [PATCH 07/25] fix methodename that handles post request --- .../basisdatenservice/rest/handbuch/HandbuchController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java index 8365345b3..527ddffbe 100644 --- a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java @@ -41,7 +41,7 @@ public ResponseEntity getHandbuch(@PathVariable("wahltagID") String wahl } @PostMapping("{wahltagID}/{wahlbezirksart}") - public void getHandbuch(@PathVariable("wahltagID") String wahltagID, @PathVariable("wahlbezirksart") WahlbezirkArtDTO wahlbezirkArtDTO, + public void setHandbuch(@PathVariable("wahltagID") String wahltagID, @PathVariable("wahlbezirksart") WahlbezirkArtDTO wahlbezirkArtDTO, final MultipartHttpServletRequest request) { try { val handbuchData = getHandbuchFromRequest(request); From 5ef15c129d5f235bd16302de14fde83df6f93e8c Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Wed, 10 Jul 2024 17:05:12 +0200 Subject: [PATCH 08/25] add lombok No/AllArgs and builder --- .../wahllokalsystem/basisdatenservice/domain/Handbuch.java | 2 ++ .../basisdatenservice/domain/WahltagIdUndWahlbezirksart.java | 4 ++++ .../services/handbuch/HandbuchReferenceModel.java | 2 ++ .../services/handbuch/HandbuchWriteModel.java | 2 ++ 4 files changed, 10 insertions(+) diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/Handbuch.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/Handbuch.java index f506dbad1..084d1e0b7 100644 --- a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/Handbuch.java +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/Handbuch.java @@ -4,6 +4,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.Lob; import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; import lombok.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; @@ -14,6 +15,7 @@ @Getter @Setter @NoArgsConstructor +@AllArgsConstructor @EqualsAndHashCode @ToString(onlyExplicitlyIncluded = true) public class Handbuch { diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/WahltagIdUndWahlbezirksart.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/WahltagIdUndWahlbezirksart.java index 88aa4fec4..7f1b2f661 100644 --- a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/WahltagIdUndWahlbezirksart.java +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/domain/WahltagIdUndWahlbezirksart.java @@ -4,9 +4,13 @@ import jakarta.persistence.Enumerated; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; import lombok.Data; +import lombok.NoArgsConstructor; @Data +@NoArgsConstructor +@AllArgsConstructor public class WahltagIdUndWahlbezirksart { @NotNull diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchReferenceModel.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchReferenceModel.java index f113786f0..1aeb09ad8 100644 --- a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchReferenceModel.java +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchReferenceModel.java @@ -1,7 +1,9 @@ package de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch; import jakarta.validation.constraints.NotNull; +import lombok.Builder; +@Builder public record HandbuchReferenceModel(@NotNull String wahltagID, @NotNull WahlbezirkArtModel wahlbezirksart) { } diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchWriteModel.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchWriteModel.java index ff0da690e..89ce65c66 100644 --- a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchWriteModel.java +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchWriteModel.java @@ -1,7 +1,9 @@ package de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch; import jakarta.validation.constraints.NotNull; +import lombok.Builder; +@Builder public record HandbuchWriteModel(@NotNull HandbuchReferenceModel handbuchReferenceModel, @NotNull byte[] handbuchData) { } From 42ebf96020a47a1ed63d485432d756655052615a Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Wed, 10 Jul 2024 17:06:13 +0200 Subject: [PATCH 09/25] create tests for handbuche classes in service package --- .../handbuch/HandbuchModelMapperTest.java | 63 +++++++ .../handbuch/HandbuchServiceSecurityTest.java | 98 +++++++++++ .../handbuch/HandbuchServiceTest.java | 119 +++++++++++++ .../handbuch/HandbuchValidatorTest.java | 158 ++++++++++++++++++ .../basisdatenservice/utils/Authorities.java | 15 ++ 5 files changed, 453 insertions(+) create mode 100644 wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchModelMapperTest.java create mode 100644 wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchServiceSecurityTest.java create mode 100644 wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchServiceTest.java create mode 100644 wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchModelMapperTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchModelMapperTest.java new file mode 100644 index 000000000..8f2c506aa --- /dev/null +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchModelMapperTest.java @@ -0,0 +1,63 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch; + +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.Handbuch; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.WahlbezirkArt; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.WahltagIdUndWahlbezirksart; +import lombok.val; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mapstruct.factory.Mappers; + +class HandbuchModelMapperTest { + + private final HandbuchModelMapper unitUnderTest = Mappers.getMapper(HandbuchModelMapper.class); + + @Nested + class ToEntityID { + + @Test + void isMapped() { + val modelToMap = new HandbuchReferenceModel("wahltagID", WahlbezirkArtModel.UWB); + + val result = unitUnderTest.toEntityID(modelToMap); + + val expectedResult = new WahltagIdUndWahlbezirksart("wahltagID", WahlbezirkArt.UWB); + Assertions.assertThat(result).isEqualTo(expectedResult); + } + + @ParameterizedTest + @EnumSource(WahlbezirkArtModel.class) + void allWahlbezirksArtEnumValuesAreMapped(final WahlbezirkArtModel wahlbezirkArtModel) { + Assertions.assertThat(unitUnderTest.toEntityID(new HandbuchReferenceModel("", wahlbezirkArtModel)).getWahlbezirksart().toString()) + .isEqualTo(wahlbezirkArtModel.toString()); + } + } + + @Nested + class ToEntity { + + @Test + void isMapped() { + val modelToMap = new HandbuchWriteModel(new HandbuchReferenceModel("wahltagID", WahlbezirkArtModel.BWB), "helloWorld".getBytes()); + + val result = unitUnderTest.toEntity(modelToMap); + + val expectedResult = new Handbuch(new WahltagIdUndWahlbezirksart("wahltagID", WahlbezirkArt.BWB), "helloWorld".getBytes()); + Assertions.assertThat(result).isEqualTo(expectedResult); + } + + @ParameterizedTest + @EnumSource(WahlbezirkArtModel.class) + void allWahlbezirksArtEnumValuesAreMapped(final WahlbezirkArtModel wahlbezirkArtModel) { + val modelToMap = new HandbuchWriteModel(new HandbuchReferenceModel("", wahlbezirkArtModel), "".getBytes()); + + val result = unitUnderTest.toEntity(modelToMap); + + Assertions.assertThat(result.getWahltagIdUndWahlbezirksart().getWahlbezirksart().toString()).isEqualTo(wahlbezirkArtModel.toString()); + } + } + +} \ No newline at end of file diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchServiceSecurityTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchServiceSecurityTest.java new file mode 100644 index 000000000..01e800d21 --- /dev/null +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchServiceSecurityTest.java @@ -0,0 +1,98 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch; + +import de.muenchen.oss.wahllokalsystem.basisdatenservice.MicroServiceApplication; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.Handbuch; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.HandbuchRepository; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.WahlbezirkArt; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.WahltagIdUndWahlbezirksart; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.utils.Authorities; +import de.muenchen.oss.wahllokalsystem.wls.common.exception.TechnischeWlsException; +import de.muenchen.oss.wahllokalsystem.wls.common.testing.SecurityUtils; +import java.util.stream.Stream; +import lombok.val; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.aggregator.ArgumentsAccessor; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.core.context.SecurityContextHolder; + +@SpringBootTest(classes = MicroServiceApplication.class) +public class HandbuchServiceSecurityTest { + + @Autowired + HandbuchService handbuchService; + + @Autowired + HandbuchRepository handbuchRepository; + + @BeforeEach + void setup() { + SecurityUtils.runWith(Authorities.REPOSITORY_DELETE_HANDBUCH); + handbuchRepository.deleteAll(); + SecurityContextHolder.clearContext(); + } + + @Nested + class GetHandbuch { + + @Test + void accessGranted() { + SecurityUtils.runWith(Authorities.REPOSITORY_WRITE_HANDBUCH); + handbuchRepository.save(new Handbuch(new WahltagIdUndWahlbezirksart("wahltagID", WahlbezirkArt.UWB), "handbuch".getBytes())); + + SecurityUtils.runWith(Authorities.ALL_AUTHORITIES_GET_HANDBUCH); + + val handbuchID = new HandbuchReferenceModel("wahltagID", WahlbezirkArtModel.UWB); + Assertions.assertThatNoException().isThrownBy(() -> handbuchService.getHandbuch(handbuchID)); + } + + @ParameterizedTest(name = "{index} - {1} missing") + @MethodSource("getMissingAuthoritiesVariations") + void anyMissingAuthorityCausesFail(final ArgumentsAccessor argumentsAccessor) { + SecurityUtils.runWith(argumentsAccessor.get(0, String[].class)); + + val handbuchID = new HandbuchReferenceModel("wahltagID", WahlbezirkArtModel.UWB); + Assertions.assertThatThrownBy(() -> handbuchService.getHandbuch(handbuchID)).isInstanceOf(AccessDeniedException.class); + } + + private static Stream getMissingAuthoritiesVariations() { + return SecurityUtils.buildArgumentsForMissingAuthoritiesVariations(Authorities.ALL_AUTHORITIES_GET_HANDBUCH); + } + + } + + @Nested + class SetHandbuch { + + @Test + void accessGranted() { + SecurityUtils.runWith(Authorities.ALL_AUTHORITIES_POST_HANDBUCH); + + val handbuchModelToSave = new HandbuchWriteModel(new HandbuchReferenceModel("wahltagID", WahlbezirkArtModel.UWB), "handbuch".getBytes()); + Assertions.assertThatNoException().isThrownBy(() -> handbuchService.setHandbuch(handbuchModelToSave)); + } + + @Test + void accessDeniedWhenServiceAuthoritiyIsMissing() { + SecurityUtils.runWith(Authorities.REPOSITORY_WRITE_HANDBUCH); + + val handbuchModelToSave = new HandbuchWriteModel(new HandbuchReferenceModel("wahltagID", WahlbezirkArtModel.UWB), "handbuch".getBytes()); + Assertions.assertThatThrownBy(() -> handbuchService.setHandbuch(handbuchModelToSave)).isInstanceOf(AccessDeniedException.class); + } + + @Test + void technischeWlsExceptionWhenRepoAuthorityIsMissing() { + SecurityUtils.runWith(Authorities.SERVICE_POST_HANDBUCH); + + val handbuchModelToSave = new HandbuchWriteModel(new HandbuchReferenceModel("wahltagID", WahlbezirkArtModel.UWB), "handbuch".getBytes()); + Assertions.assertThatThrownBy(() -> handbuchService.setHandbuch(handbuchModelToSave)).isInstanceOf(TechnischeWlsException.class); + } + } +} diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchServiceTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchServiceTest.java new file mode 100644 index 000000000..93aaf0257 --- /dev/null +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchServiceTest.java @@ -0,0 +1,119 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch; + +import static org.mockito.Mockito.times; + +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.Handbuch; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.HandbuchRepository; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.WahltagIdUndWahlbezirksart; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.exception.ExceptionConstants; +import de.muenchen.oss.wahllokalsystem.wls.common.exception.TechnischeWlsException; +import de.muenchen.oss.wahllokalsystem.wls.common.exception.util.ExceptionFactory; +import java.util.Optional; +import lombok.val; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HandbuchServiceTest { + + @Mock + HandbuchValidator handbuchValidator; + + @Mock + HandbuchModelMapper handbuchModelMapper; + + @Mock + HandbuchRepository handbuchRepository; + + @Mock + ExceptionFactory exceptionFactory; + + @InjectMocks + HandbuchService unitUnderTest; + + @Nested + class GetHandbuch { + + @Test + void dataFound() { + val handbuchReference = new HandbuchReferenceModel("wahltagID", WahlbezirkArtModel.UWB); + + val mockedHandbuchID = new WahltagIdUndWahlbezirksart(); + val mockedRepoResponse = new Handbuch(null, "handbuch.txt".getBytes()); + + Mockito.when(handbuchModelMapper.toEntityID(handbuchReference)).thenReturn(mockedHandbuchID); + Mockito.when(handbuchRepository.findById(mockedHandbuchID)).thenReturn(Optional.of(mockedRepoResponse)); + + val result = unitUnderTest.getHandbuch(handbuchReference); + + Assertions.assertThat(result).isEqualTo("handbuch.txt".getBytes()); + } + + @Test + void noDataFound() { + val handbuchReference = new HandbuchReferenceModel("wahltagID", WahlbezirkArtModel.UWB); + + val mockedHandbuchID = new WahltagIdUndWahlbezirksart(); + val mockedException = TechnischeWlsException.withCode("").buildWithMessage(""); + + Mockito.when(handbuchModelMapper.toEntityID(handbuchReference)).thenReturn(mockedHandbuchID); + Mockito.when(handbuchRepository.findById(mockedHandbuchID)).thenReturn(Optional.empty()); + Mockito.when(exceptionFactory.createTechnischeWlsException(ExceptionConstants.GETHANDBUCH_KEINE_DATEN)).thenReturn(mockedException); + + Assertions.assertThatThrownBy(() -> unitUnderTest.getHandbuch(handbuchReference)).isSameAs(mockedException); + } + } + + @Nested + class SetHandbuch { + + @Test + void dataSuccessfullySaved() { + val handbuchToSave = HandbuchWriteModel.builder().build(); + + val mockedModelMappedToEntity = new Handbuch(); + + Mockito.when(handbuchModelMapper.toEntity(handbuchToSave)).thenReturn(mockedModelMappedToEntity); + + Assertions.assertThatNoException().isThrownBy(() -> unitUnderTest.setHandbuch(handbuchToSave)); + + Mockito.verify(handbuchRepository).save(mockedModelMappedToEntity); + } + + @Test + void noSaveOnValidationError() { + val handbuchToSave = HandbuchWriteModel.builder().build(); + + val mockedValidationException = new RuntimeException("validation failed"); + + Mockito.doThrow(mockedValidationException).when(handbuchValidator).validHandbuchWriteModelOrThrow(handbuchToSave); + + Assertions.assertThatThrownBy(() -> unitUnderTest.setHandbuch(handbuchToSave)).isSameAs(mockedValidationException); + + Mockito.verify(handbuchRepository, times(0)).save(Mockito.any()); + } + + @Test + void onSaveExceptionIsMappedToWlsException() { + val handbuchToSave = HandbuchWriteModel.builder().build(); + + val mockedWlsException = TechnischeWlsException.withCode("").buildWithMessage(""); + val mockedOnSaveException = new RuntimeException("saving failed"); + val mockedModelMappedToEntity = new Handbuch(); + + Mockito.when(handbuchModelMapper.toEntity(handbuchToSave)).thenReturn(mockedModelMappedToEntity); + Mockito.when(exceptionFactory.createTechnischeWlsException(ExceptionConstants.POSTHANDBUCH_SPEICHERN_NICHT_ERFOLGREICH)) + .thenReturn(mockedWlsException); + Mockito.doThrow(mockedOnSaveException).when(handbuchRepository).save(mockedModelMappedToEntity); + + Assertions.assertThatThrownBy(() -> unitUnderTest.setHandbuch(handbuchToSave)).isSameAs(mockedWlsException); + } + } + +} \ No newline at end of file diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java new file mode 100644 index 000000000..34bdb2d93 --- /dev/null +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java @@ -0,0 +1,158 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch; + +import de.muenchen.oss.wahllokalsystem.basisdatenservice.exception.ExceptionConstants; +import de.muenchen.oss.wahllokalsystem.wls.common.exception.FachlicheWlsException; +import de.muenchen.oss.wahllokalsystem.wls.common.exception.util.ExceptionFactory; +import lombok.val; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class HandbuchValidatorTest { + + @Mock + ExceptionFactory exceptionFactory; + + @InjectMocks + HandbuchValidator unitUnderTest; + + @Nested + class ValidHandbuchReferenceOrThrow { + + private final FachlicheWlsException mockedWlsException = FachlicheWlsException.withCode("").buildWithMessage(""); + + @Test + void noExceptionWhenModelIsValid() { + val validModel = initValidModel().build(); + + Assertions.assertThatNoException().isThrownBy(() -> unitUnderTest.validHandbuchReferenceOrThrow(validModel)); + } + + @Test + void exceptionWhenModelIsNull() { + Mockito.when(exceptionFactory.createFachlicheWlsException(ExceptionConstants.GETHANDBUCH_PARAMETER_UNVOLLSTAENDIG)).thenReturn(mockedWlsException); + + Assertions.assertThatThrownBy(() -> unitUnderTest.validHandbuchReferenceOrThrow(null)).isSameAs(mockedWlsException); + } + + @Test + void exceptionWhenWahltagIDIsNull() { + val invalidModel = initValidModel().wahltagID(null).build(); + + Mockito.when(exceptionFactory.createFachlicheWlsException(ExceptionConstants.GETHANDBUCH_PARAMETER_UNVOLLSTAENDIG)).thenReturn(mockedWlsException); + + Assertions.assertThatThrownBy(() -> unitUnderTest.validHandbuchReferenceOrThrow(invalidModel)).isSameAs(mockedWlsException); + } + + @Test + void exceptionWhenWahltagIDIsEmptyString() { + val invalidModel = initValidModel().wahltagID("").build(); + + Mockito.when(exceptionFactory.createFachlicheWlsException(ExceptionConstants.GETHANDBUCH_PARAMETER_UNVOLLSTAENDIG)).thenReturn(mockedWlsException); + + Assertions.assertThatThrownBy(() -> unitUnderTest.validHandbuchReferenceOrThrow(invalidModel)).isSameAs(mockedWlsException); + } + + @Test + void exceptionWhenWahltagIDIsBlankString() { + val invalidModel = initValidModel().wahltagID(" ").build(); + + Mockito.when(exceptionFactory.createFachlicheWlsException(ExceptionConstants.GETHANDBUCH_PARAMETER_UNVOLLSTAENDIG)).thenReturn(mockedWlsException); + + Assertions.assertThatThrownBy(() -> unitUnderTest.validHandbuchReferenceOrThrow(invalidModel)).isSameAs(mockedWlsException); + } + + @Test + void exceptionWhenWahlbezirksArtIsNull() { + val invalidModel = initValidModel().wahlbezirksart(null).build(); + + Mockito.when(exceptionFactory.createFachlicheWlsException(ExceptionConstants.GETHANDBUCH_PARAMETER_UNVOLLSTAENDIG)).thenReturn(mockedWlsException); + + Assertions.assertThatThrownBy(() -> unitUnderTest.validHandbuchReferenceOrThrow(invalidModel)).isSameAs(mockedWlsException); + } + + private HandbuchReferenceModel.HandbuchReferenceModelBuilder initValidModel() { + return HandbuchReferenceModel.builder().wahlbezirksart(WahlbezirkArtModel.BWB).wahltagID("wahltagID"); + } + } + + @Nested + class ValidHandbuchWriteModelOrThrow { + + private final FachlicheWlsException mockedWlsException = FachlicheWlsException.withCode("").buildWithMessage(""); + + @Test + void noExceptionWhenModelIsValid() { + val validModel = initValidModel().build(); + + Assertions.assertThatNoException().isThrownBy(() -> unitUnderTest.validHandbuchWriteModelOrThrow(validModel)); + } + + @Test + void exceptionWhenModelIsNull() { + Mockito.when(exceptionFactory.createFachlicheWlsException(ExceptionConstants.POSTHANDBUCH_PARAMETER_UNVOLLSTAENDIG)).thenReturn(mockedWlsException); + + Assertions.assertThatThrownBy(() -> unitUnderTest.validHandbuchWriteModelOrThrow(null)).isSameAs(mockedWlsException); + } + + @Test + void exceptionWhenHandbuchReferenceIsNull() { + val invalidModel = initValidModel().handbuchReferenceModel(null).build(); + + Mockito.when(exceptionFactory.createFachlicheWlsException(ExceptionConstants.POSTHANDBUCH_PARAMETER_UNVOLLSTAENDIG)).thenReturn(mockedWlsException); + + Assertions.assertThatThrownBy(() -> unitUnderTest.validHandbuchWriteModelOrThrow(invalidModel)).isSameAs(mockedWlsException); + } + + @Test + void exceptionWhenHandbuchReferenceWahltagIDIsNull() { + val invalidModel = initValidModel().handbuchReferenceModel(initValidHandbuchReferenceModel().wahltagID(null).build()).build(); + + Mockito.when(exceptionFactory.createFachlicheWlsException(ExceptionConstants.POSTHANDBUCH_PARAMETER_UNVOLLSTAENDIG)).thenReturn(mockedWlsException); + + Assertions.assertThatThrownBy(() -> unitUnderTest.validHandbuchWriteModelOrThrow(invalidModel)).isSameAs(mockedWlsException); + } + + @Test + void exceptionWhenHandbuchReferenceWahltagIDIsEmptyString() { + val invalidModel = initValidModel().handbuchReferenceModel(initValidHandbuchReferenceModel().wahltagID("").build()).build(); + + Mockito.when(exceptionFactory.createFachlicheWlsException(ExceptionConstants.POSTHANDBUCH_PARAMETER_UNVOLLSTAENDIG)).thenReturn(mockedWlsException); + + Assertions.assertThatThrownBy(() -> unitUnderTest.validHandbuchWriteModelOrThrow(invalidModel)).isSameAs(mockedWlsException); + } + + @Test + void exceptionWhenHandbuchReferenceWahltagIDIsBlankString() { + val invalidModel = initValidModel().handbuchReferenceModel(initValidHandbuchReferenceModel().wahltagID(" ").build()).build(); + + Mockito.when(exceptionFactory.createFachlicheWlsException(ExceptionConstants.POSTHANDBUCH_PARAMETER_UNVOLLSTAENDIG)).thenReturn(mockedWlsException); + + Assertions.assertThatThrownBy(() -> unitUnderTest.validHandbuchWriteModelOrThrow(invalidModel)).isSameAs(mockedWlsException); + } + + @Test + void exceptionWhenHandbuchReferenceWahlbezirksArtIsNull() { + val invalidModel = initValidModel().handbuchReferenceModel(initValidHandbuchReferenceModel().wahlbezirksart(null).build()).build(); + + Mockito.when(exceptionFactory.createFachlicheWlsException(ExceptionConstants.POSTHANDBUCH_PARAMETER_UNVOLLSTAENDIG)).thenReturn(mockedWlsException); + + Assertions.assertThatThrownBy(() -> unitUnderTest.validHandbuchWriteModelOrThrow(invalidModel)).isSameAs(mockedWlsException); + + } + + private HandbuchWriteModel.HandbuchWriteModelBuilder initValidModel() { + return HandbuchWriteModel.builder().handbuchReferenceModel(initValidHandbuchReferenceModel().build()); + } + + private HandbuchReferenceModel.HandbuchReferenceModelBuilder initValidHandbuchReferenceModel() { + return HandbuchReferenceModel.builder().wahlbezirksart(WahlbezirkArtModel.BWB).wahltagID("wahltagID"); + } + } +} \ No newline at end of file diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/utils/Authorities.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/utils/Authorities.java index e5f3b74d3..fe4f7f709 100644 --- a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/utils/Authorities.java +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/utils/Authorities.java @@ -8,6 +8,9 @@ public class Authorities { public static final String SERVICE_GET_WAHLVORSCHLAEGE = "Basisdaten_BUSINESSACTION_GetWahlvorschlaege"; + public static final String SERVICE_GET_HANDBUCH = "Basisdaten_BUSINESSACTION_GetHandbuch"; + public static final String SERVICE_POST_HANDBUCH = "Basisdaten_BUSINESSACTION_PostHandbuch"; + public static final String REPOSITORY_READ_WAHLVORSCHLAEGE = "Basisdaten_READ_WLSWahlvorschlaege"; public static final String REPOSITORY_DELETE_WAHLVORSCHLAEGE = "Basisdaten_DELETE_WLSWahlvorschlaege"; public static final String REPOSITORY_WRITE_WAHLVORSCHLAEGE = "Basisdaten_WRITE_WLSWahlvorschlaege"; @@ -20,6 +23,10 @@ public class Authorities { public static final String REPOSITORY_WRITE_KANDIDAT = "Basisdaten_WRITE_Kandidat"; public static final String REPOSITORY_DELETE_KANDIDAT = "Basisdaten_DELETE_Kandidat"; + public static final String REPOSITORY_READ_HANDBUCH = "Basisdaten_READ_Handbuch"; + public static final String REPOSITORY_WRITE_HANDBUCH = "Basisdaten_WRITE_Handbuch"; + public static final String REPOSITORY_DELETE_HANDBUCH = "Basisdaten_DELETE_Handbuch"; + public static final String[] ALL_AUTHORITIES_GET_WAHLVORSCHLAEGE = new String[] { SERVICE_GET_WAHLVORSCHLAEGE, REPOSITORY_READ_WAHLVORSCHLAEGE, @@ -36,5 +43,13 @@ public class Authorities { public static final String[] ALL_AUTHORITIES_DELETE_WAHLVORSCHLAEGE = new String[] { REPOSITORY_DELETE_WAHLVORSCHLAEGE }; + public static final String[] ALL_AUTHORITIES_GET_HANDBUCH = { + SERVICE_GET_HANDBUCH, + REPOSITORY_READ_HANDBUCH, + }; + public static final String[] ALL_AUTHORITIES_POST_HANDBUCH = { + SERVICE_POST_HANDBUCH, + REPOSITORY_WRITE_HANDBUCH + }; } From df68f753f29fda58d20df88e4e887021a42511c2 Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Wed, 10 Jul 2024 17:06:51 +0200 Subject: [PATCH 10/25] draft handbuch controller unit test --- .../rest/handbuch/HandbuchControllerTest.java | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerTest.java diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerTest.java new file mode 100644 index 000000000..d0dfef47b --- /dev/null +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerTest.java @@ -0,0 +1,94 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.rest.handbuch; + +import static org.mockito.ArgumentMatchers.eq; + +import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchReferenceModel; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchService; +import de.muenchen.oss.wahllokalsystem.wls.common.exception.util.ExceptionFactory; +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; +import lombok.val; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.multipart.support.DefaultMultipartHttpServletRequest; + +@ExtendWith(MockitoExtension.class) +class HandbuchControllerTest { + + @Mock + HandbuchService handbuchService; + + @Mock + ExceptionFactory exceptionFactory; + + @Mock + HandbuchDTOMapper handbuchDTOMapper; + + @InjectMocks + HandbuchController unitUnderTest; + + @Nested + class GetHandbuch { + + @Test + void serviceIsCalled() { + val filenameSuffix = "file.pdf"; + val wahltagID = "wahltagID"; + val wahlbezirkArt = WahlbezirkArtDTO.UWB; + + val mockedHandbuchReferenceModel = HandbuchReferenceModel.builder().build(); + val mockedServiceResponse = "response".getBytes(); + + Mockito.when(handbuchDTOMapper.toModel(eq(wahltagID), eq(wahlbezirkArt))).thenReturn(mockedHandbuchReferenceModel); + Mockito.when(handbuchService.getHandbuch(mockedHandbuchReferenceModel)).thenReturn(mockedServiceResponse); + + unitUnderTest.manualFileNameSuffix = filenameSuffix; + + val result = unitUnderTest.getHandbuch(wahltagID, wahlbezirkArt); + + val expectedHeaders = new HttpHeaders(); + expectedHeaders.add("Content-Type", "application/pdf"); + expectedHeaders.add("Content-Disposition", "attachment; filename=UWB" + filenameSuffix); + val expectedResult = new ResponseEntity(mockedServiceResponse, expectedHeaders, HttpStatus.OK); + Assertions.assertThat(result).isEqualTo(expectedResult); + } + } + + @Nested + class SetHandbuch { + + @Test + void requestIsSendToService() { + val fileContent = "helloMyLovelyTestcase".getBytes(); + val multiPartFiles = new LinkedMultiValueMap(); + multiPartFiles.put("key", List.of(new MockMultipartFile("filename", fileContent))); + final HttpServletRequest httpServletRequest = Mockito.mock(HttpServletRequest.class); + val servletRequest = new DefaultMultipartHttpServletRequest(httpServletRequest, multiPartFiles, null, null); + + unitUnderTest.setHandbuch(null, null, servletRequest); + } + + @Test + void exceptionWhenRequestHasNoAttachment() { + + } + + @Test + void anyServiceExceptionIsMappedToTechnischeWlsException() { + + } + } + +} \ No newline at end of file From 7306176daef037da54d8f7b01fa4d3f7485eaf70 Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:48:55 +0200 Subject: [PATCH 11/25] Finalize controller unit test --- .../rest/handbuch/HandbuchControllerTest.java | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerTest.java index d0dfef47b..d660d04bb 100644 --- a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerTest.java +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerTest.java @@ -2,10 +2,14 @@ import static org.mockito.ArgumentMatchers.eq; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.exception.ExceptionConstants; import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchReferenceModel; import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchService; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchWriteModel; +import de.muenchen.oss.wahllokalsystem.wls.common.exception.TechnischeWlsException; import de.muenchen.oss.wahllokalsystem.wls.common.exception.util.ExceptionFactory; import jakarta.servlet.http.HttpServletRequest; +import java.util.Collections; import java.util.List; import lombok.val; import org.assertj.core.api.Assertions; @@ -77,17 +81,31 @@ void requestIsSendToService() { final HttpServletRequest httpServletRequest = Mockito.mock(HttpServletRequest.class); val servletRequest = new DefaultMultipartHttpServletRequest(httpServletRequest, multiPartFiles, null, null); - unitUnderTest.setHandbuch(null, null, servletRequest); + val mockedHandbuchReferenceModel = HandbuchReferenceModel.builder().build(); + val mockedHandbuchWriteModel = HandbuchWriteModel.builder().build(); + + Mockito.when(handbuchDTOMapper.toModel(eq("wahltagID"), eq(WahlbezirkArtDTO.UWB))).thenReturn(mockedHandbuchReferenceModel); + Mockito.when(handbuchDTOMapper.toModel(eq(mockedHandbuchReferenceModel), eq(fileContent))).thenReturn(mockedHandbuchWriteModel); + + unitUnderTest.setHandbuch("wahltagID", WahlbezirkArtDTO.UWB, servletRequest); + + Mockito.verify(handbuchService).setHandbuch(mockedHandbuchWriteModel); } @Test void exceptionWhenRequestHasNoAttachment() { + val multiPartFiles = new LinkedMultiValueMap(); + multiPartFiles.put("key", Collections.emptyList()); + final HttpServletRequest httpServletRequest = Mockito.mock(HttpServletRequest.class); + val servletRequest = new DefaultMultipartHttpServletRequest(httpServletRequest, multiPartFiles, null, null); - } + val mockedTechnischeWlsException = TechnischeWlsException.withCode("").buildWithMessage(""); - @Test - void anyServiceExceptionIsMappedToTechnischeWlsException() { + Mockito.when(exceptionFactory.createTechnischeWlsException(ExceptionConstants.POSTHANDBUCH_SPEICHERN_NICHT_ERFOLGREICH)) + .thenReturn(mockedTechnischeWlsException); + Assertions.assertThatThrownBy(() -> unitUnderTest.setHandbuch("wahltagID", WahlbezirkArtDTO.UWB, servletRequest)) + .isSameAs(mockedTechnischeWlsException); } } From 92ad04cdac1b4ff72c47d3d367ea3888ffa2cc76 Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:49:29 +0200 Subject: [PATCH 12/25] implemented controller integration test --- .../HandbuchControllerIntegretionTest.java | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerIntegretionTest.java diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerIntegretionTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerIntegretionTest.java new file mode 100644 index 000000000..3670dfac3 --- /dev/null +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerIntegretionTest.java @@ -0,0 +1,134 @@ +package de.muenchen.oss.wahllokalsystem.basisdatenservice.rest.handbuch; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.MicroServiceApplication; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.Handbuch; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.HandbuchRepository; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.WahlbezirkArt; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.domain.WahltagIdUndWahlbezirksart; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.exception.ExceptionConstants; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.utils.Authorities; +import de.muenchen.oss.wahllokalsystem.wls.common.exception.rest.model.WlsExceptionCategory; +import de.muenchen.oss.wahllokalsystem.wls.common.exception.rest.model.WlsExceptionDTO; +import de.muenchen.oss.wahllokalsystem.wls.common.exception.util.ExceptionKonstanten; +import de.muenchen.oss.wahllokalsystem.wls.common.testing.SecurityUtils; +import lombok.val; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +@SpringBootTest(classes = MicroServiceApplication.class) +@AutoConfigureMockMvc +public class HandbuchControllerIntegretionTest { + + @Value("${service.info.oid}") + String serviceOid; + + @Autowired + MockMvc mockMvc; + + @Autowired + ObjectMapper objectMapper; + + @Autowired + HandbuchRepository handbuchRepository; + + @AfterEach + void tearDown() { + SecurityUtils.runWith(Authorities.REPOSITORY_DELETE_HANDBUCH); + handbuchRepository.deleteAll(); + } + + @Nested + class GetHandbuch { + + @Test + void dataFound() throws Exception { + val handbuchContent = "dies ist ein Handbuch".getBytes(); + + SecurityUtils.runWith(Authorities.REPOSITORY_WRITE_HANDBUCH); + handbuchRepository.save(new Handbuch(new WahltagIdUndWahlbezirksart("wahltagID", WahlbezirkArt.UWB), handbuchContent)); + + SecurityUtils.runWith(Authorities.ALL_AUTHORITIES_GET_HANDBUCH); + val request = MockMvcRequestBuilders.get("/businessActions/handbuch/wahltagID/UWB"); + val response = mockMvc.perform(request).andExpect(status().isOk()).andReturn(); + val responseBodyAsByteArray = response.getResponse().getContentAsByteArray(); + + Assertions.assertThat(response.getResponse().getHeader("Content-Type")).isEqualTo("application/pdf"); + Assertions.assertThat(response.getResponse().getHeader("Content-Disposition")).isEqualTo("attachment; filename=UWBHandbuch.pdf"); + Assertions.assertThat(responseBodyAsByteArray).isEqualTo(handbuchContent); + } + + @Test + void noDataFound() throws Exception { + SecurityUtils.runWith(Authorities.ALL_AUTHORITIES_GET_HANDBUCH); + val request = MockMvcRequestBuilders.get("/businessActions/handbuch/wahltagID/UWB"); + val response = mockMvc.perform(request).andExpect(status().isInternalServerError()).andReturn(); + + val responseBodyAsWlsExceptionDTO = objectMapper.readValue(response.getResponse().getContentAsString(), WlsExceptionDTO.class); + + val expectedWlsExceptionDTO = new WlsExceptionDTO(WlsExceptionCategory.T, ExceptionConstants.GETHANDBUCH_KEINE_DATEN.code(), serviceOid, + ExceptionConstants.GETHANDBUCH_KEINE_DATEN.message()); + Assertions.assertThat(responseBodyAsWlsExceptionDTO).isEqualTo(expectedWlsExceptionDTO); + } + + } + + @Nested + class SetHandbuch { + + @Test + void dataIsSaved() throws Exception { + val handbuchContent = "dies ist ein Handbuch".getBytes(); + + SecurityUtils.runWith(Authorities.ALL_AUTHORITIES_POST_HANDBUCH); + val request = MockMvcRequestBuilders.multipart("/businessActions/handbuch/wahltagID/UWB").file("manual", handbuchContent).with(csrf()); + mockMvc.perform(request).andExpect(status().isOk()); + + SecurityUtils.runWith(Authorities.REPOSITORY_READ_HANDBUCH); + val savedHandbuch = handbuchRepository.findById(new WahltagIdUndWahlbezirksart("wahltagID", WahlbezirkArt.UWB)).get(); + + Assertions.assertThat(savedHandbuch.getHandbuch()).isEqualTo(handbuchContent); + } + + @Test + void existingDataIsReplaced() throws Exception { + SecurityUtils.runWith(Authorities.REPOSITORY_WRITE_HANDBUCH); + handbuchRepository.save(new Handbuch(new WahltagIdUndWahlbezirksart("wahltagID", WahlbezirkArt.UWB), "alter handbuch content".getBytes())); + + val handbuchContent = "dies ist ein Handbuch".getBytes(); + + SecurityUtils.runWith(Authorities.ALL_AUTHORITIES_POST_HANDBUCH); + val request = MockMvcRequestBuilders.multipart("/businessActions/handbuch/wahltagID/UWB").file("manual", handbuchContent).with(csrf()); + mockMvc.perform(request).andExpect(status().isOk()); + + SecurityUtils.runWith(Authorities.REPOSITORY_READ_HANDBUCH); + val savedHandbuch = handbuchRepository.findById(new WahltagIdUndWahlbezirksart("wahltagID", WahlbezirkArt.UWB)).get(); + + Assertions.assertThat(savedHandbuch.getHandbuch()).isEqualTo(handbuchContent); + } + + @Test + void wlsExceptionOccuredCauseOfMissingAttachment() throws Exception { + SecurityUtils.runWith(Authorities.ALL_AUTHORITIES_POST_HANDBUCH); + val request = MockMvcRequestBuilders.multipart("/businessActions/handbuch/wahltagID/UWB").with(csrf()); + val response = mockMvc.perform(request).andExpect(status().isInternalServerError()).andReturn(); + val responseBodyAsWlsExceptionDTO = objectMapper.readValue(response.getResponse().getContentAsString(), WlsExceptionDTO.class); + + val expectedWlsExceptionDTO = new WlsExceptionDTO(WlsExceptionCategory.T, ExceptionKonstanten.CODE_ALLGEMEIN_UNBEKANNT, + serviceOid, ""); + + Assertions.assertThat(responseBodyAsWlsExceptionDTO).usingRecursiveComparison().ignoringFields("message").isEqualTo(expectedWlsExceptionDTO); + } + } +} From 736ffd484bf536fa3933c1dfd595ac3f6b95a950 Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:49:57 +0200 Subject: [PATCH 13/25] updated securityconfigurationtest for handbuch --- .../SecurityConfigurationTest.java | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/configuration/SecurityConfigurationTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/configuration/SecurityConfigurationTest.java index 2aa68c8cd..e72f07b38 100644 --- a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/configuration/SecurityConfigurationTest.java +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/configuration/SecurityConfigurationTest.java @@ -1,10 +1,14 @@ package de.muenchen.oss.wahllokalsystem.basisdatenservice.configuration; import static de.muenchen.oss.wahllokalsystem.basisdatenservice.TestConstants.SPRING_TEST_PROFILE; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import de.muenchen.oss.wahllokalsystem.basisdatenservice.MicroServiceApplication; +import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchService; import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.wahlvorschlag.WahlvorschlaegeService; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -30,6 +34,9 @@ class SecurityConfigurationTest { @MockBean WahlvorschlaegeService wahlvorschlaegeService; + @MockBean + HandbuchService handbuchService; + @Test void accessSecuredResourceRootThenUnauthorized() throws Exception { api.perform(get("/")) @@ -88,4 +95,32 @@ void accessGetWahlvorstaendeUnauthorizedThenOk() throws Exception { } } + @Nested + class Handbuch { + + @Test + @WithAnonymousUser + void accessGetHandbuchUnauthorizedThenUnauthorized() throws Exception { + api.perform(get("/businessActions/handbuch/wahlID/UWB")).andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser + void accessGetHandbuchUnauthorizedThenOk() throws Exception { + api.perform(get("/businessActions/handbuch/wahlID/UWB")).andExpect(status().isOk()); + } + + @Test + @WithAnonymousUser + void accessPostHandbuchUnauthorizedThenUnauthorized() throws Exception { + api.perform(post("/businessActions/handbuch/wahlID/UWB").with(csrf())).andExpect(status().isUnauthorized()); + } + + @Test + @WithMockUser + void accessPostHandbuchUnauthorizedThenOk() throws Exception { + api.perform(multipart("/businessActions/handbuch/wahlID/UWB").file("manual", "content".getBytes()).with(csrf())).andExpect(status().isOk()); + } + } + } From bf15e35de93e7cf6fd5bf0617fafeed89f99b812 Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Wed, 10 Jul 2024 18:50:28 +0200 Subject: [PATCH 14/25] spotless:apply --- .../basisdatenservice/rest/handbuch/HandbuchControllerTest.java | 2 +- .../services/handbuch/HandbuchModelMapperTest.java | 2 +- .../services/handbuch/HandbuchServiceTest.java | 2 +- .../services/handbuch/HandbuchValidatorTest.java | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerTest.java index d660d04bb..edca1b0c6 100644 --- a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerTest.java +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerTest.java @@ -109,4 +109,4 @@ void exceptionWhenRequestHasNoAttachment() { } } -} \ No newline at end of file +} diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchModelMapperTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchModelMapperTest.java index 8f2c506aa..4a4f5ff5f 100644 --- a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchModelMapperTest.java +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchModelMapperTest.java @@ -60,4 +60,4 @@ void allWahlbezirksArtEnumValuesAreMapped(final WahlbezirkArtModel wahlbezirkArt } } -} \ No newline at end of file +} diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchServiceTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchServiceTest.java index 93aaf0257..33f2cd025 100644 --- a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchServiceTest.java +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchServiceTest.java @@ -116,4 +116,4 @@ void onSaveExceptionIsMappedToWlsException() { } } -} \ No newline at end of file +} diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java index 34bdb2d93..b12f7d070 100644 --- a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java @@ -155,4 +155,4 @@ private HandbuchReferenceModel.HandbuchReferenceModelBuilder initValidHandbuchRe return HandbuchReferenceModel.builder().wahlbezirksart(WahlbezirkArtModel.BWB).wahltagID("wahltagID"); } } -} \ No newline at end of file +} From 1e2a7035ce5d5c181488221805f6ea3a981d9d56 Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Wed, 10 Jul 2024 19:04:22 +0200 Subject: [PATCH 15/25] add openApiDoc for handbuch --- .../rest/handbuch/HandbuchController.java | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java index 527ddffbe..ba2f88e94 100644 --- a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java @@ -2,7 +2,12 @@ import de.muenchen.oss.wahllokalsystem.basisdatenservice.exception.ExceptionConstants; import de.muenchen.oss.wahllokalsystem.basisdatenservice.services.handbuch.HandbuchService; +import de.muenchen.oss.wahllokalsystem.wls.common.exception.rest.model.WlsExceptionDTO; import de.muenchen.oss.wahllokalsystem.wls.common.exception.util.ExceptionFactory; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; import java.io.IOException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -34,6 +39,19 @@ public class HandbuchController { private final HandbuchDTOMapper handbuchDTOMapper; @GetMapping("{wahltagID}/{wahlbezirksart}") + @Operation( + description = "Abrufen des Handbuches für eine Wahl für eine bestimmte Wahlbezirksart", + responses = { + @ApiResponse( + responseCode = "500", description = "Handbuch ist nicht abrufbar. Entweder fehlt es oder es gab technische Probleme", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WlsExceptionDTO.class)) + ), + @ApiResponse( + responseCode = "400", description = "Anfrageparameter sind fehlerhaft", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WlsExceptionDTO.class)) + ) + } + ) public ResponseEntity getHandbuch(@PathVariable("wahltagID") String wahltagID, @PathVariable("wahlbezirksart") WahlbezirkArtDTO wahlbezirkArtDTO) { val handbuchData = handbuchService.getHandbuch(handbuchDTOMapper.toModel(wahltagID, wahlbezirkArtDTO)); @@ -41,6 +59,16 @@ public ResponseEntity getHandbuch(@PathVariable("wahltagID") String wahl } @PostMapping("{wahltagID}/{wahlbezirksart}") + @Operation( + description = "Speichern eines Handbuches für eine Wahl für eine bestimmte Wahlbezirksart", + responses = { + @ApiResponse(responseCode = "500", description = "Handbuch nicht speicherbar"), + @ApiResponse( + responseCode = "400", description = "Anfrageparameter sind fehlerhaft", + content = @Content(mediaType = "application/json", schema = @Schema(implementation = WlsExceptionDTO.class)) + ) + } + ) public void setHandbuch(@PathVariable("wahltagID") String wahltagID, @PathVariable("wahlbezirksart") WahlbezirkArtDTO wahlbezirkArtDTO, final MultipartHttpServletRequest request) { try { From f32fe428190d1e965a9bb0346572816c4c89f4f2 Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Wed, 10 Jul 2024 19:11:00 +0200 Subject: [PATCH 16/25] add handbuch keycloak migration authorities --- .../add-authorities-basisdaten-handbuch.yml | 48 +++++++++++++++++++ .../keycloak/migration/keycloak-changelog.yml | 3 +- 2 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 stack/keycloak/migration/add-authorities-basisdaten-handbuch.yml diff --git a/stack/keycloak/migration/add-authorities-basisdaten-handbuch.yml b/stack/keycloak/migration/add-authorities-basisdaten-handbuch.yml new file mode 100644 index 000000000..5b569e174 --- /dev/null +++ b/stack/keycloak/migration/add-authorities-basisdaten-handbuch.yml @@ -0,0 +1,48 @@ +id: add authorities basisdaten handbuch +author: MrSebastian +realm: ${SSO_REALM} +changes: + - addRole: + name: Basisdaten_BUSINESSACTION_GetHandbuch + clientRole: true + clientId: ${SSO_CLIENT_ID} + - assignRoleToGroup: + group: allBasisdatenAuthorities + role: Basisdaten_BUSINESSACTION_GetHandbuch + clientId: ${SSO_CLIENT_ID} + + - addRole: + name: Basisdaten_BUSINESSACTION_PostHandbuch + clientRole: true + clientId: ${SSO_CLIENT_ID} + - assignRoleToGroup: + group: allBasisdatenAuthorities + role: Basisdaten_BUSINESSACTION_PostHandbuch + clientId: ${SSO_CLIENT_ID} + + - addRole: + name: Basisdaten_READ_Handbuch + clientRole: true + clientId: ${SSO_CLIENT_ID} + - assignRoleToGroup: + group: allBasisdatenAuthorities + role: Basisdaten_READ_Handbuch + clientId: ${SSO_CLIENT_ID} + + - addRole: + name: Basisdaten_WRITE_Handbuch + clientRole: true + clientId: ${SSO_CLIENT_ID} + - assignRoleToGroup: + group: allBasisdatenAuthorities + role: Basisdaten_WRITE_Handbuch + clientId: ${SSO_CLIENT_ID} + + - addRole: + name: Basisdaten_DELETE_Handbuch + clientRole: true + clientId: ${SSO_CLIENT_ID} + - assignRoleToGroup: + group: allBasisdatenAuthorities + role: Basisdaten_DELETE_Handbuch + clientId: ${SSO_CLIENT_ID} \ No newline at end of file diff --git a/stack/keycloak/migration/keycloak-changelog.yml b/stack/keycloak/migration/keycloak-changelog.yml index 31513aef6..1678f8876 100644 --- a/stack/keycloak/migration/keycloak-changelog.yml +++ b/stack/keycloak/migration/keycloak-changelog.yml @@ -30,4 +30,5 @@ includes: - path: add-authorities-eai-wahlvorstand.yml - path: add-authorities-wahlvorbereitung-briefwahlvorbereitung.yml - path: create-group-all-basisdaten-authorities.yml - - path: add-authorities-basisdaten-wahlvorschlaege.yml \ No newline at end of file + - path: add-authorities-basisdaten-wahlvorschlaege.yml + - path: add-authorities-basisdaten-handbuch.yml \ No newline at end of file From 54f6afe8af2bcc080cd1d30da97feba073c18e3e Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Wed, 10 Jul 2024 19:11:52 +0200 Subject: [PATCH 17/25] add oracle flyway file for handbuch --- .../db/migrations/oracle/V2_0__createHandbuchTable.sql | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 wls-basisdaten-service/src/main/resources/db/migrations/oracle/V2_0__createHandbuchTable.sql diff --git a/wls-basisdaten-service/src/main/resources/db/migrations/oracle/V2_0__createHandbuchTable.sql b/wls-basisdaten-service/src/main/resources/db/migrations/oracle/V2_0__createHandbuchTable.sql new file mode 100644 index 000000000..be74d84dd --- /dev/null +++ b/wls-basisdaten-service/src/main/resources/db/migrations/oracle/V2_0__createHandbuchTable.sql @@ -0,0 +1,8 @@ +CREATE TABLE Handbuch +( + wahltagid VARCHAR(1024) not null, + wahlbezirksart VARCHAR(255) not null, + handbuch blob not null, + + primary key (wahltagid, wahlbezirksart) +); From 5848a4724d59dbb333ec5618e2e426ceff4435d1 Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Wed, 10 Jul 2024 19:17:14 +0200 Subject: [PATCH 18/25] add doc for manual --- docs/src/features/basisdaten-service/index.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/src/features/basisdaten-service/index.md b/docs/src/features/basisdaten-service/index.md index 462b82067..b7147616e 100644 --- a/docs/src/features/basisdaten-service/index.md +++ b/docs/src/features/basisdaten-service/index.md @@ -7,6 +7,7 @@ Service zur Bereitstellung folgender Basisdaten: - Wahlbezirke - Wahlvorschläge - Kopfdaten +- Handbuch Wahlen, Wahlbezirke und Kopfdaten können in der Service-Datenbank gespeichert werden. @@ -14,4 +15,10 @@ Wahlen, Wahlbezirke und Kopfdaten können in der Service-Datenbank gespeichert w Folgende Services werden zum Betrieb benötigt: - EAI-Service -- Infomanagement-Service \ No newline at end of file +- Infomanagement-Service + +## Handbuch + +In dem Service werden Handbücher verwaltet. Je Wahl und Wahlbezirkart kann ein Handbuch hinterlegt werden. + +Bei dem Handbuch soll es sich um ein PDF-Dokument handeln. \ No newline at end of file From a91419c21a69690831a7e67c9b66bb0e1a416cfa Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Thu, 11 Jul 2024 15:59:35 +0200 Subject: [PATCH 19/25] feedback: improve endpoint description --- .../basisdatenservice/rest/handbuch/HandbuchController.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java index ba2f88e94..edc421941 100644 --- a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchController.java @@ -40,7 +40,7 @@ public class HandbuchController { @GetMapping("{wahltagID}/{wahlbezirksart}") @Operation( - description = "Abrufen des Handbuches für eine Wahl für eine bestimmte Wahlbezirksart", + description = "Abrufen des Handbuches einer Wahl für eine bestimmte Wahlbezirksart", responses = { @ApiResponse( responseCode = "500", description = "Handbuch ist nicht abrufbar. Entweder fehlt es oder es gab technische Probleme", @@ -60,9 +60,9 @@ public ResponseEntity getHandbuch(@PathVariable("wahltagID") String wahl @PostMapping("{wahltagID}/{wahlbezirksart}") @Operation( - description = "Speichern eines Handbuches für eine Wahl für eine bestimmte Wahlbezirksart", + description = "Speichern eines Handbuches einer Wahl für eine bestimmte Wahlbezirksart", responses = { - @ApiResponse(responseCode = "500", description = "Handbuch nicht speicherbar"), + @ApiResponse(responseCode = "500", description = "Handbuch kann nicht gespeichert werden"), @ApiResponse( responseCode = "400", description = "Anfrageparameter sind fehlerhaft", content = @Content(mediaType = "application/json", schema = @Schema(implementation = WlsExceptionDTO.class)) From ec65d7f6724c1dde22e5ad51a50b3c5791b9ca7e Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:04:23 +0200 Subject: [PATCH 20/25] fix typo in testclassname --- ...egretionTest.java => HandbuchControllerIntegrationTest.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/{HandbuchControllerIntegretionTest.java => HandbuchControllerIntegrationTest.java} (99%) diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerIntegretionTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerIntegrationTest.java similarity index 99% rename from wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerIntegretionTest.java rename to wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerIntegrationTest.java index 3670dfac3..e264632f9 100644 --- a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerIntegretionTest.java +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/rest/handbuch/HandbuchControllerIntegrationTest.java @@ -29,7 +29,7 @@ @SpringBootTest(classes = MicroServiceApplication.class) @AutoConfigureMockMvc -public class HandbuchControllerIntegretionTest { +public class HandbuchControllerIntegrationTest { @Value("${service.info.oid}") String serviceOid; From 6ae8b5ba24de2a3b2ed10779925805df6f01dbb9 Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:06:25 +0200 Subject: [PATCH 21/25] Apply suggestions from code review feedback: fix test names Co-authored-by: Robert Jasny --- .../configuration/SecurityConfigurationTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/configuration/SecurityConfigurationTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/configuration/SecurityConfigurationTest.java index e72f07b38..ee6181c6c 100644 --- a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/configuration/SecurityConfigurationTest.java +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/configuration/SecurityConfigurationTest.java @@ -106,7 +106,7 @@ void accessGetHandbuchUnauthorizedThenUnauthorized() throws Exception { @Test @WithMockUser - void accessGetHandbuchUnauthorizedThenOk() throws Exception { + void accessGetHandbuchAuthorizedThenOk() throws Exception { api.perform(get("/businessActions/handbuch/wahlID/UWB")).andExpect(status().isOk()); } @@ -118,7 +118,7 @@ void accessPostHandbuchUnauthorizedThenUnauthorized() throws Exception { @Test @WithMockUser - void accessPostHandbuchUnauthorizedThenOk() throws Exception { + void accessPostHandbuchAuthorizedThenOk() throws Exception { api.perform(multipart("/businessActions/handbuch/wahlID/UWB").file("manual", "content".getBytes()).with(csrf())).andExpect(status().isOk()); } } From f0f558dd43c7403a9ee571dae5c708246e3a3e2d Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:07:32 +0200 Subject: [PATCH 22/25] Apply suggestions from code review: fix id -> ID feedback: fix id -> ID Co-authored-by: Robert Jasny --- .../resources/db/migrations/h2/V2_0__createHandbuchTable.sql | 2 +- .../db/migrations/oracle/V2_0__createHandbuchTable.sql | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/wls-basisdaten-service/src/main/resources/db/migrations/h2/V2_0__createHandbuchTable.sql b/wls-basisdaten-service/src/main/resources/db/migrations/h2/V2_0__createHandbuchTable.sql index 7eebfc8bf..19a9bda74 100644 --- a/wls-basisdaten-service/src/main/resources/db/migrations/h2/V2_0__createHandbuchTable.sql +++ b/wls-basisdaten-service/src/main/resources/db/migrations/h2/V2_0__createHandbuchTable.sql @@ -1,6 +1,6 @@ CREATE TABLE Handbuch ( - wahltagid varchar(1024) not null, + wahltagID varchar(1024) not null, wahlbezirksart varchar(255) not null, handbuch blob not null, diff --git a/wls-basisdaten-service/src/main/resources/db/migrations/oracle/V2_0__createHandbuchTable.sql b/wls-basisdaten-service/src/main/resources/db/migrations/oracle/V2_0__createHandbuchTable.sql index be74d84dd..2b452d522 100644 --- a/wls-basisdaten-service/src/main/resources/db/migrations/oracle/V2_0__createHandbuchTable.sql +++ b/wls-basisdaten-service/src/main/resources/db/migrations/oracle/V2_0__createHandbuchTable.sql @@ -1,8 +1,8 @@ CREATE TABLE Handbuch ( - wahltagid VARCHAR(1024) not null, + wahltagID VARCHAR(1024) not null, wahlbezirksart VARCHAR(255) not null, handbuch blob not null, - primary key (wahltagid, wahlbezirksart) + primary key (wahltagID, wahlbezirksart) ); From 12eb3a6846fad2b4e296c21a86a97814de38432e Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:11:38 +0200 Subject: [PATCH 23/25] fix: use same column name in primary key --- .../resources/db/migrations/h2/V2_0__createHandbuchTable.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wls-basisdaten-service/src/main/resources/db/migrations/h2/V2_0__createHandbuchTable.sql b/wls-basisdaten-service/src/main/resources/db/migrations/h2/V2_0__createHandbuchTable.sql index 19a9bda74..c3835755e 100644 --- a/wls-basisdaten-service/src/main/resources/db/migrations/h2/V2_0__createHandbuchTable.sql +++ b/wls-basisdaten-service/src/main/resources/db/migrations/h2/V2_0__createHandbuchTable.sql @@ -3,6 +3,6 @@ CREATE TABLE Handbuch wahltagID varchar(1024) not null, wahlbezirksart varchar(255) not null, handbuch blob not null, - - primary key (wahltagid, wahlbezirksart) + + primary key (wahltagID, wahlbezirksart) ); From f3bed1aa1ce103aabc215ee8a609bc582bc1bb59 Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:18:53 +0200 Subject: [PATCH 24/25] feedback: add check that handbuchdata contains data --- .../services/handbuch/HandbuchValidator.java | 6 +++++- .../handbuch/HandbuchValidatorTest.java | 20 ++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidator.java b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidator.java index 10e6024b4..324375b71 100644 --- a/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidator.java +++ b/wls-basisdaten-service/src/main/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidator.java @@ -3,6 +3,7 @@ import de.muenchen.oss.wahllokalsystem.basisdatenservice.exception.ExceptionConstants; import de.muenchen.oss.wahllokalsystem.wls.common.exception.util.ExceptionFactory; import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; @@ -20,7 +21,10 @@ public void validHandbuchReferenceOrThrow(final HandbuchReferenceModel handbuchR public void validHandbuchWriteModelOrThrow(final HandbuchWriteModel handbuchWriteModel) { if (handbuchWriteModel == null || handbuchWriteModel.handbuchReferenceModel() == null || StringUtils.isBlank( - handbuchWriteModel.handbuchReferenceModel().wahltagID()) || handbuchWriteModel.handbuchReferenceModel().wahlbezirksart() == null) { + handbuchWriteModel.handbuchReferenceModel().wahltagID()) + || handbuchWriteModel.handbuchReferenceModel() + .wahlbezirksart() == null + || ArrayUtils.isEmpty(handbuchWriteModel.handbuchData())) { throw exceptionFactory.createFachlicheWlsException(ExceptionConstants.POSTHANDBUCH_PARAMETER_UNVOLLSTAENDIG); } } diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java index b12f7d070..0a727e107 100644 --- a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java @@ -147,8 +147,26 @@ void exceptionWhenHandbuchReferenceWahlbezirksArtIsNull() { } + @Test + void exceptionWhenHandbuchDataIsNull() { + val invalidModel = initValidModel().handbuchData(new byte[0]).build(); + + Mockito.when(exceptionFactory.createFachlicheWlsException(ExceptionConstants.POSTHANDBUCH_PARAMETER_UNVOLLSTAENDIG)).thenReturn(mockedWlsException); + + Assertions.assertThatThrownBy(() -> unitUnderTest.validHandbuchWriteModelOrThrow(invalidModel)).isSameAs(mockedWlsException); + } + + @Test + void exceptionWhenHandbuchDataHasZeroLength() { + val invalidModel = initValidModel().handbuchData(new byte[0]).build(); + + Mockito.when(exceptionFactory.createFachlicheWlsException(ExceptionConstants.POSTHANDBUCH_PARAMETER_UNVOLLSTAENDIG)).thenReturn(mockedWlsException); + + Assertions.assertThatThrownBy(() -> unitUnderTest.validHandbuchWriteModelOrThrow(invalidModel)).isSameAs(mockedWlsException); + } + private HandbuchWriteModel.HandbuchWriteModelBuilder initValidModel() { - return HandbuchWriteModel.builder().handbuchReferenceModel(initValidHandbuchReferenceModel().build()); + return HandbuchWriteModel.builder().handbuchReferenceModel(initValidHandbuchReferenceModel().build()).handbuchData("text".getBytes()); } private HandbuchReferenceModel.HandbuchReferenceModelBuilder initValidHandbuchReferenceModel() { From d3bf5fccfb8bd5e9106c9ee8a310c344856e36bf Mon Sep 17 00:00:00 2001 From: MrSebastian <13592751+MrSebastian@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:19:20 +0200 Subject: [PATCH 25/25] remove empty line at end of method --- .../services/handbuch/HandbuchValidatorTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java index 0a727e107..5bdc861fd 100644 --- a/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java +++ b/wls-basisdaten-service/src/test/java/de/muenchen/oss/wahllokalsystem/basisdatenservice/services/handbuch/HandbuchValidatorTest.java @@ -144,7 +144,6 @@ void exceptionWhenHandbuchReferenceWahlbezirksArtIsNull() { Mockito.when(exceptionFactory.createFachlicheWlsException(ExceptionConstants.POSTHANDBUCH_PARAMETER_UNVOLLSTAENDIG)).thenReturn(mockedWlsException); Assertions.assertThatThrownBy(() -> unitUnderTest.validHandbuchWriteModelOrThrow(invalidModel)).isSameAs(mockedWlsException); - } @Test