diff --git a/server/src/main/helm/templates/deployment.yaml b/server/src/main/helm/templates/deployment.yaml index 874205860d1689b61d879b4b154621a5dfa57337..f70d00df638dc54df685ac47cc30a0bee74046d1 100644 --- a/server/src/main/helm/templates/deployment.yaml +++ b/server/src/main/helm/templates/deployment.yaml @@ -121,6 +121,11 @@ spec: - name: SPRING_SECURITY_SAML2_RELYINGPARTY_REGISTRATION_BAYERNID_ENTITY-ID value: {{ required ".Values.antragsraum.saml.entityID must be set" .Values.antragsraum.saml.entityID }} + - name: SPRING_SERVLET_MULTIPART_MAX-FILE-SIZE + value: {{.Values.antragsraum.servlet.maxFileSize }} + - name: SPRING_SERVLET_MULTIPART_MAX-REQUEST-SIZE + value: {{.Values.antragsraum.servlet.maxRequestSize }} + - name: OZGCLOUD_JWT_SECRET valueFrom: secretKeyRef: diff --git a/server/src/main/helm/values.yaml b/server/src/main/helm/values.yaml index c048a4516ad24a0eb06a18a41ae66c39422f4156..ddea7310a55a48d50ade5c970e73a0d46acdb0e3 100644 --- a/server/src/main/helm/values.yaml +++ b/server/src/main/helm/values.yaml @@ -30,4 +30,9 @@ image: repo: dockerproxy.ozg-sh.de name: bitnami/kubectl tag: latest -replicaCount: 1 \ No newline at end of file +replicaCount: 1 + +antragsraum: + servlet: + maxRequestSize: 50MB + maxFileSize: 30MB diff --git a/server/src/main/java/de/ozgcloud/antragsraum/SecurityConfiguration.java b/server/src/main/java/de/ozgcloud/antragsraum/SecurityConfiguration.java index 22d5858da91b5398bdfc300baca1523795069b01..77cf8627f99d762fed6a68edd572fd767a0efc83 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/SecurityConfiguration.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/SecurityConfiguration.java @@ -34,6 +34,8 @@ import org.springframework.security.saml2.provider.service.web.authentication.Sa import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; +import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @@ -53,6 +55,7 @@ import lombok.RequiredArgsConstructor; public class SecurityConfiguration { private static final String OPTIONS = "OPTIONS"; private static final String GET = "GET"; + private static final String POLICY_DIRECTIVES = "object-src 'none'; child-src 'self'; frame-ancestors 'none'; base-uri 'none'; upgrade-insecure-requests; require-trusted-types-for 'script'"; @NonNull private final BayernIdSaml2Extension bayernIdSaml2Extension; @@ -70,6 +73,11 @@ public class SecurityConfiguration { return new SecurityProvider(); } + @Bean + public SecurityContextLogoutHandler logoutHandler() { + return new SecurityContextLogoutHandler(); + } + @Bean Saml2AuthenticationRequestResolver authenticationRequestResolver(RelyingPartyRegistrationRepository registrations) { var registrationResolver = new DefaultRelyingPartyRegistrationResolver(registrations); @@ -91,7 +99,11 @@ public class SecurityConfiguration { .requestMatchers("/api/**").authenticated() .anyRequest().denyAll()) .addFilterBefore(tokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) - .csrf(AbstractHttpConfigurer::disable); + .csrf(AbstractHttpConfigurer::disable) + .headers(headers -> headers.contentSecurityPolicy( + csp -> csp.policyDirectives(POLICY_DIRECTIVES))) + .headers(headers -> headers.referrerPolicy( + referrerPolicyConfig -> referrerPolicyConfig.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.NO_REFERRER))); http.saml2Login(samlLogin -> samlLogin.successHandler(getSamlUrlAuthenticationSuccessHandler())) .saml2Logout(Customizer.withDefaults()) diff --git a/server/src/main/java/de/ozgcloud/antragsraum/attachments/FileRestClient.java b/server/src/main/java/de/ozgcloud/antragsraum/attachments/FileRestClient.java index de48e9faee253c06dda24fe03085c8458284ce83..bfde3830060e9a10f1123032e12ce9898e28f2e7 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/attachments/FileRestClient.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/attachments/FileRestClient.java @@ -48,9 +48,12 @@ import lombok.extern.log4j.Log4j2; @Log4j2 @Component class FileRestClient { - private static final String FIND_FILE_METADATA_API = "/api/v1/file/metadata/{fileId}"; - private static final String GET_FILE_CONTENT_API = "/api/v1/file/content/{fileId}"; + static final String FIND_FILE_METADATA_API = "/api/v1/file/metadata/{fileId}"; + static final String GET_FILE_CONTENT_API = "/api/v1/file/content/{fileId}"; static final String UPLOAD_FILE_API = "/api/v1/file"; + public static final String FILE_KEY = "file"; + public static final String VORGANG_ID_KEY = "vorgangId"; + static final String NO_FILE_ID_RECEIVED_MESSAGE = "No fileId received"; private final RestClient restClient; @@ -74,7 +77,7 @@ class FileRestClient { if (Objects.nonNull(response)) { fileIdFuture.complete(response); } else { - fileIdFuture.completeExceptionally(new IllegalStateException("No fileId received")); + fileIdFuture.completeExceptionally(new IllegalStateException(NO_FILE_ID_RECEIVED_MESSAGE)); } } catch (Exception e) { fileIdFuture.completeExceptionally(e); @@ -83,19 +86,16 @@ class FileRestClient { private String sendFile(OzgUploadFile uploadFile, String address) { MultiValueMap<String, Object> requestBody = new LinkedMultiValueMap<>(); - requestBody.add("file", uploadFile.file().getResource()); - requestBody.add("vorgangId", uploadFile.vorgangId()); + requestBody.add(FILE_KEY, uploadFile.file().getResource()); + requestBody.add(VORGANG_ID_KEY, uploadFile.vorgangId()); - var fileId = restClient + return restClient .post() .uri(UPLOAD_FILE_API) .header(X_GRPC_ADDRESS, RestClientUtils.sanitizeAddress(address)) .contentType(MediaType.MULTIPART_FORM_DATA) .body(requestBody) .retrieve().toEntity(String.class).getBody(); - LOG.info("received {}", fileId); - - return fileId; } void downloadFileContent(final String fileId, final OutputStream out, final String address) { diff --git a/server/src/main/java/de/ozgcloud/antragsraum/attachments/VirusScannerClient.java b/server/src/main/java/de/ozgcloud/antragsraum/attachments/VirusScannerClient.java index f2ac82063ad66c8bb1d77e3bcce3409422352276..4085c550fd58f8bd9597731b265cea39e6941109 100755 --- a/server/src/main/java/de/ozgcloud/antragsraum/attachments/VirusScannerClient.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/attachments/VirusScannerClient.java @@ -57,7 +57,7 @@ class VirusScannerClient { Set<String> viruses = new HashSet<>(); if (Objects.nonNull(file)) { - LOG.info("Scanning file {} for viruses using ClamAV at {}", file.getOriginalFilename(), clamAVScanUrl); + LOG.debug("Scanning file {} for viruses using ClamAV at {}", file.getOriginalFilename(), clamAVScanUrl); MultiValueMap<String, Object> requestBody = new LinkedMultiValueMap<>(); requestBody.add(FILE_REQUEST_KEY, file.getResource()); diff --git a/server/src/main/java/de/ozgcloud/antragsraum/command/CommandRestClient.java b/server/src/main/java/de/ozgcloud/antragsraum/command/CommandRestClient.java index e25c388d258eb28792286e7e4f55860dde4a76a7..8bb9b294d01055b9bbf6ac6549e07b08c4007f56 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/command/CommandRestClient.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/command/CommandRestClient.java @@ -34,7 +34,7 @@ import lombok.extern.log4j.Log4j2; @Log4j2 @Component class CommandRestClient { - private static final String COMMAND_URI = "/api/v1/command/{commandId}"; + static final String COMMAND_URI = "/api/v1/command/{commandId}"; private final RestClient restClient; diff --git a/server/src/main/java/de/ozgcloud/antragsraum/common/InvalidFileAdvice.java b/server/src/main/java/de/ozgcloud/antragsraum/common/InvalidFileAdvice.java index 375e174891763498e30b3b9ef01bae4e8e9c776d..e24bea7c2cd28b6e85cf79cc0d37d40c86a69552 100755 --- a/server/src/main/java/de/ozgcloud/antragsraum/common/InvalidFileAdvice.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/common/InvalidFileAdvice.java @@ -21,7 +21,7 @@ package de.ozgcloud.antragsraum.common; import jakarta.annotation.Priority; -import lombok.extern.log4j.Log4j2; + import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -30,13 +30,15 @@ import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.context.request.WebRequest; import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; +import lombok.extern.log4j.Log4j2; + @ControllerAdvice @Log4j2 @Priority(value = 0) public class InvalidFileAdvice extends ResponseEntityExceptionHandler { - @ExceptionHandler(value = {InvalidFileTypeException.class}) - protected ResponseEntity<Object> handleVirusFound(RuntimeException ex, WebRequest request) { - LOG.error("Uploaded file is not valid.", ex); - return handleExceptionInternal(ex, "File not acceptable", new HttpHeaders(), HttpStatus.NOT_ACCEPTABLE, request); - } + @ExceptionHandler(value = { InvalidFileTypeException.class }) + protected ResponseEntity<Object> handleInvalidFile(RuntimeException ex, WebRequest request) { + LOG.error("Uploaded file is not valid.", ex); + return handleExceptionInternal(ex, "File not acceptable", new HttpHeaders(), HttpStatus.NOT_ACCEPTABLE, request); + } } diff --git a/server/src/main/java/de/ozgcloud/antragsraum/common/logging/RepositoryAspectPointcut.java b/server/src/main/java/de/ozgcloud/antragsraum/common/logging/RepositoryAspectPointcut.java new file mode 100644 index 0000000000000000000000000000000000000000..47f0bcdda4fca053d060bed639c87e17a27441aa --- /dev/null +++ b/server/src/main/java/de/ozgcloud/antragsraum/common/logging/RepositoryAspectPointcut.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2022 Das Land Schleswig-Holstein vertreten durch den + * Ministerpräsidenten des Landes Schleswig-Holstein + * Staatskanzlei + * Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.antragsraum.common.logging; + +import org.aspectj.lang.annotation.Pointcut; + +public class RepositoryAspectPointcut { + + @Pointcut("execution(public * org.springframework.data.repository.Repository+.*(..))") + void anyPublicRepositoryMethod() { + // aspect pointcut - no implementation needed + } +} diff --git a/server/src/main/java/de/ozgcloud/antragsraum/common/logging/RepositoryLoggingAspect.java b/server/src/main/java/de/ozgcloud/antragsraum/common/logging/RepositoryLoggingAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..efb41906f2b1771743030b3fd3fdbd6d50aaa9c9 --- /dev/null +++ b/server/src/main/java/de/ozgcloud/antragsraum/common/logging/RepositoryLoggingAspect.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2024. Das Land Schleswig-Holstein vertreten durch den Ministerpräsidenten + * des Landes Schleswig-Holstein Staatskanzlei Abteilung Digitalisierung und zentrales IT-Management der Landesregierung. + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.antragsraum.common.logging; + +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Before; +import org.springframework.stereotype.Component; + +import de.ozgcloud.common.logging.AspectLoggingUtils; + +@Aspect +@Component +public class RepositoryLoggingAspect extends RepositoryAspectPointcut { + + @Before("anyPublicRepositoryMethod()") + public void onRepositoryMethod(JoinPoint joinPoint) { + AspectLoggingUtils.log(joinPoint); + } +} \ No newline at end of file diff --git a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/Nachricht.java b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/Nachricht.java index cd43487035b44a0f60aab17490a6b9a627c6343c..6d762929c6627ff46c0185b01c58e75cb594c938 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/Nachricht.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/Nachricht.java @@ -100,4 +100,23 @@ record Nachricht( type = "String" ) String status) { + + @Override + public String toString() { + return "Nachricht{" + + "id='" + id + '\'' + + ", nachrichtEventId='" + nachrichtEventId + '\'' + + ", answeredAt=" + answeredAt + + ", sendAt=" + sendAt + + ", title='" + title + '\'' + + ", message=<...masked for privacy..>" + + ", postfachId='" + postfachId + '\'' + + ", vorgangId='" + vorgangId + '\'' + + ", replyOption=" + replyOption + + ", attachments=" + attachments + + ", accessible=" + accessible + + ", trustLevel='" + trustLevel + '\'' + + ", status='" + status + '\'' + + '}'; + } } diff --git a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtHeaderMapper.java b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtHeaderMapper.java index 25c943faa4422982d4119fde729967e2e34f6252..032a097056123aa9e662f0375df4b78221aa6bed 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtHeaderMapper.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtHeaderMapper.java @@ -24,6 +24,8 @@ import java.time.DateTimeException; import java.time.LocalDateTime; import java.time.ZoneOffset; +import org.apache.commons.lang3.StringUtils; + import de.ozgcloud.antragsraum.proxy.AntragraumproxyGrpcRueckfrageHead; import lombok.AccessLevel; import lombok.NoArgsConstructor; @@ -37,14 +39,17 @@ class NachrichtHeaderMapper { try { timestamp = LocalDateTime.parse(dateString).toEpochSecond(ZoneOffset.UTC) * 1000; } catch (DateTimeException e) { - LOG.warn("Invalid date [{}] received for Nachricht. Setting it to 0 (=1970-01-01)", dateString); + if (StringUtils.isEmpty(dateString)) { + LOG.debug("Empty date received for Nachricht. Setting it to 0 (=1970-01-01)"); + } else { + LOG.warn("Invalid date [{}] received for Nachricht. Setting it to 0 (=1970-01-01)", dateString); + } } return timestamp; } static NachrichtHeader fromRueckfrageHead(final AntragraumproxyGrpcRueckfrageHead rueckfrageHead, String nachrichtEventId) { - LOG.info("Mapping RueckfrageHead [{}] for NachrichtEvent [{}] to NachrichtHeader", rueckfrageHead, nachrichtEventId); var builder = NachrichtHeader.builder() .vorgangId(rueckfrageHead.getVorgangId()) .id(rueckfrageHead.getId()) diff --git a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtMapper.java b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtMapper.java index 88c9e6ca9582fd25e37621dbec660708f3404b69..a118da2f45e0537d03bf6173cf7e869c72a7db43 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtMapper.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtMapper.java @@ -57,14 +57,17 @@ class NachrichtMapper { try { timestamp = LocalDateTime.parse(dateString).toEpochSecond(ZoneOffset.UTC) * 1000; } catch (DateTimeException e) { - LOG.warn("Invalid date [{}] received for Nachricht. Setting it to 0 (=1970-01-01)", dateString); + if (StringUtils.isEmpty(dateString)) { + LOG.debug("Empty date received for Nachricht. Setting it to 0 (=1970-01-01)"); + } else { + LOG.warn("Invalid date [{}] received for Nachricht. Setting it to 0 (=1970-01-01)", dateString); + } } return timestamp; } public Nachricht fromRestRueckfrage(final AntragraumproxyGrpcRueckfrage rueckfrage, final String id) { - LOG.info("Mapping RestRueckfrage [{}] for NachrichtEvent [{}] to Nachricht", rueckfrage, id); var builder = Nachricht.builder() .vorgangId(rueckfrage.getVorgangId()) .id(rueckfrage.getId()) diff --git a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRemoteService.java b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRemoteService.java index fef102a521f04f1f9ca7b48e16bc2d447faf3c9d..3fb1be48928fdafb737c6d1771a0d35dc34366da 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRemoteService.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRemoteService.java @@ -21,6 +21,7 @@ package de.ozgcloud.antragsraum.nachricht; import java.util.List; +import java.util.Objects; import java.util.Optional; import org.springframework.stereotype.Service; @@ -30,7 +31,6 @@ import de.ozgcloud.antragsraum.events.NachrichtEvent; import de.ozgcloud.antragsraum.proxy.AntragraumproxyGrpcSendRueckfrageAnswerResponse; import de.ozgcloud.antragsraum.security.AuthenticationHelper; import io.grpc.StatusRuntimeException; -import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @@ -38,17 +38,17 @@ import lombok.extern.log4j.Log4j2; @Log4j2 @RequiredArgsConstructor class NachrichtenRemoteService { - private final @NonNull NachrichtenRestClient nachrichtenRestClient; - private final @NonNull NachrichtMapper nachrichtMapper; - private final @NonNull AntragraumProxyMapper antragraumProxyMapper; + private final NachrichtenRestClient nachrichtenRestClient; + private final NachrichtMapper nachrichtMapper; + private final AntragraumProxyMapper antragraumProxyMapper; List<NachrichtHeader> findRueckfrageHeads(NachrichtEvent event) { try { var reply = nachrichtenRestClient.getFindRueckfragen(event.postfachId(), AuthenticationHelper.getSamlToken(), event.address()); var rueckfrageHeads = reply.getRueckfrageHead(); - LOG.info("{} RueckfrageHeads für Postfach ID {} erhalten", rueckfrageHeads.size(), event.postfachId()); - return rueckfrageHeads.stream().map(rueckfrageHead -> NachrichtHeaderMapper.fromRueckfrageHead(rueckfrageHead, event.id())).toList(); + return Objects.nonNull(rueckfrageHeads) ? rueckfrageHeads.stream() + .map(rueckfrageHead -> NachrichtHeaderMapper.fromRueckfrageHead(rueckfrageHead, event.id())).toList() : List.of(); } catch (StatusRuntimeException e) { LOG.error("Error beim Laden der RueckfrageHeads für das Postfach {}", event.postfachId(), e); } @@ -60,9 +60,8 @@ class NachrichtenRemoteService { try { var reply = nachrichtenRestClient.getGetRueckfrage(rueckfrageId, AuthenticationHelper.getSamlToken(), event.address()); var rueckfrage = reply.getRueckfrage(); - LOG.info("Rueckfrage [{}] für ID {} erhalten", rueckfrage, rueckfrageId); - return Optional.of(nachrichtMapper.fromRestRueckfrage(rueckfrage, event.id())); + return Objects.nonNull(rueckfrage) ? Optional.of(nachrichtMapper.fromRestRueckfrage(rueckfrage, event.id())) : Optional.empty(); } catch (StatusRuntimeException e) { LOG.error("Error beim Laden der Rueckfrage mit ID {}", rueckfrageId, e); } diff --git a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtenService.java b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtenService.java index 4957d99651f5c2944e7b85fb5abad73bff17cfe4..b1a9b37f695f2cfd90675e5cd719aabe8e747f28 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtenService.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/NachrichtenService.java @@ -27,10 +27,15 @@ import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; import java.util.Collection; import java.util.List; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; + import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; import de.ozgcloud.antragsraum.command.CommandReference; import de.ozgcloud.antragsraum.common.NotAccessibleException; +import de.ozgcloud.antragsraum.common.TechnicalException; import de.ozgcloud.antragsraum.events.NachrichtEvent; import de.ozgcloud.antragsraum.events.NachrichtEventService; import lombok.NonNull; @@ -39,12 +44,14 @@ import lombok.extern.log4j.Log4j2; @Log4j2 @Service +@Validated @RequiredArgsConstructor class NachrichtenService { static final String RUECKFRAGE_LINK_RELATIONSHIP_NAME = "rueckfrage"; private final @NonNull NachrichtEventService nachrichtEventService; private final @NonNull NachrichtenRemoteService nachrichtenRemoteService; + private final @NonNull Validator validator; List<RueckfrageHeader> getRueckfrageHeadersOfPostfach(String postfachId) { var headers = RueckfrageHeaderMapper.fromNachrichtHeaders(getNachrichtHeadersOfPostfach(postfachId)); @@ -91,8 +98,18 @@ class NachrichtenService { } CommandReference sendRueckfrageAnswer(final ReplyNachricht nachricht) { + validate(nachricht); + var nachrichtEvent = nachrichtEventService.getNachrichtEventById(nachricht.nachrichtEventId()); return nachrichtenRemoteService.sendAnswer(nachricht, nachrichtEvent.address()); } + + private void validate(final ReplyNachricht nachricht) { + var validationResult = validator.validate(nachricht); + + if (!validationResult.isEmpty()) { + throw new TechnicalException(new ConstraintViolationException(validationResult)); + } + } } diff --git a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/ReplyNachricht.java b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/ReplyNachricht.java index 22d1db48c6ef1c9edf758e66faaeb6a68b27c953..6c712bb36bcb222275c89a2e163f74c33d404966 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/ReplyNachricht.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/ReplyNachricht.java @@ -25,8 +25,6 @@ import java.util.List; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; -import org.springframework.validation.annotation.Validated; - import com.fasterxml.jackson.annotation.JsonInclude; import de.ozgcloud.antragsraum.attachments.OzgFile; @@ -35,24 +33,26 @@ import lombok.Builder; @JsonInclude(JsonInclude.Include.NON_NULL) @Builder(toBuilder = true) -@Validated record ReplyNachricht( + @Size(max = 36, message = "An id is too long") @Schema(description = "The id of the postfach", name = "postfachId", type = "string", example = "28721c6f-b78f-4d5c-a048-19fd2fc429d2" ) String postfachId, + @Size(max = 24, message = "An id id too long") @Schema(description = "The id of the NachrichtEvent in the InfoManager the reply belongs to", name = "nachrichtEventId", type = "string", example = "60af924b4f1a2560298b4567" ) String nachrichtEventId, - @Schema(description = "The encoded address of the OZG-Cloud instanz the reply belongs to", + @Size(max = 24, message = "An id is too long") + @Schema(description = "The id of the Rückfrage", name = "id", type = "string", - example = "c3RhdGljOi8vdXRvcGlhLnNoLm96Zy1jbG91ZC5kZTo5MDkw" + example = "66af924b4f1a2560298b45ab" ) @NotEmpty String id, @@ -61,8 +61,20 @@ record ReplyNachricht( type = "string", example = "Im Anhang sind die Dokumente" ) - @Size(max = 500000, message = "message too long") + @Size(max = 100_000, message = "Message text too long") String message, @Schema(description = "The list of the attached file ids. Only the file id is used when sending the reply, The other values are set by the backend") + @Size(max = 20) List<OzgFile> attachments) { + + @Override + public String toString() { + return "ReplyNachricht{" + + "postfachId='" + postfachId + '\'' + + ", nachrichtEventId='" + nachrichtEventId + '\'' + + ", id='" + id + '\'' + + ", attachments=" + attachments + + ", message=<...masked for privacy..>" + + '}'; + } } diff --git a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/Rueckfrage.java b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/Rueckfrage.java index 59717183afe5c98c86a254ead54cad318818ec1e..b4d28c83bf61673b91420dc13ba1509b3684aa3a 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/Rueckfrage.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/Rueckfrage.java @@ -63,4 +63,16 @@ class Rueckfrage { type = "ReplyNachricht" ) List<ReplyNachricht> antworten; + + @Override + public String toString() { + return "Rueckfrage{" + + "title='" + title + '\'' + + ", status='" + status + '\'' + + ", nachricht=<...masked for privacy..>" + + ", accessible=" + accessible + + ", trustLevel='" + trustLevel + '\'' + + ", antworten=<...masked for privacy..>" + + '}'; + } } diff --git a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/RueckfrageHeaderMapper.java b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/RueckfrageHeaderMapper.java index a69bb75c3e63bb9efcedf89b1223a57af4248937..bddbc759e27ed37a944ea46994bedb132b263e19 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/RueckfrageHeaderMapper.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/RueckfrageHeaderMapper.java @@ -37,7 +37,6 @@ class RueckfrageHeaderMapper { } private static RueckfrageHeader fromNachrichtHeader(NachrichtHeader nachrichtHeader) { - LOG.info("Mapping NachrichtHeader [{}] to RueckfrageHeader", nachrichtHeader); return RueckfrageHeader.builder() .title(nachrichtHeader.title()) .nachrichtHeader(nachrichtHeader) diff --git a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/RueckfrageMapper.java b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/RueckfrageMapper.java index 6bc1c707b2a09166962422a0547c4473151eba2f..d147533bde77d21fb1a2385df12dcbaaba24e714 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/nachricht/RueckfrageMapper.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/nachricht/RueckfrageMapper.java @@ -32,7 +32,6 @@ class RueckfrageMapper { static Rueckfrage fromNachricht(Nachricht nachricht) { var clerkNachricht = nachricht.toBuilder().replyNachrichten(null).build(); - LOG.info("Mapping Nachricht [{}] to Rueckfrage", nachricht); return Rueckfrage.builder() .title(nachricht.title()) .nachricht(clerkNachricht) diff --git a/server/src/main/java/de/ozgcloud/antragsraum/security/AuthenticationController.java b/server/src/main/java/de/ozgcloud/antragsraum/security/AuthenticationController.java index c6a4e7d12675b146c54e96bc356e112aee3361ce..4bb26808b2b6fdd104747a3703164c3edfce0ec7 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/security/AuthenticationController.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/security/AuthenticationController.java @@ -20,10 +20,15 @@ package de.ozgcloud.antragsraum.security; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -40,6 +45,7 @@ class AuthenticationController { private final InMemoryUserDetailService userDetailsService; private final JwtTokenProvider tokenProvider; + private final SecurityContextLogoutHandler logoutHandler; @PostMapping(path = "/authenticate", produces = MediaType.TEXT_PLAIN_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) ResponseEntity<String> login(@RequestBody AuthCode data) { @@ -68,8 +74,13 @@ class AuthenticationController { } @GetMapping(path = "/logout") - ResponseEntity<String> logout(@AuthenticationPrincipal User user) { - userDetailsService.logout(user); + ResponseEntity<String> logout(HttpServletRequest request, HttpServletResponse response, @AuthenticationPrincipal User user) { + if (user != null) { + userDetailsService.logout(user); + } + + var authentication = SecurityContextHolder.getContext().getAuthentication(); + logoutHandler.logout(request, response, authentication); return ResponseEntity.ok("Success"); } diff --git a/server/src/main/java/de/ozgcloud/antragsraum/security/AuthenticationHelper.java b/server/src/main/java/de/ozgcloud/antragsraum/security/AuthenticationHelper.java index c7d9ceb8bb1e560f27745b3dcc0d4fce53a48498..6f20f2ffd44b48379d2517fbe28d4c56e03c971c 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/security/AuthenticationHelper.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/security/AuthenticationHelper.java @@ -57,8 +57,6 @@ public class AuthenticationHelper { tokenValue = user.getSamlToken(); } - LOG.debug("Returning SAML Token:[{}]", tokenValue); - return tokenValue; } } diff --git a/server/src/main/java/de/ozgcloud/antragsraum/security/JwtTokenFilter.java b/server/src/main/java/de/ozgcloud/antragsraum/security/JwtTokenFilter.java index 84fb44283d5a860543c67f598bc4acf53782e07e..866c00c3968dca9881b5790ed5be38b66d3c42bc 100644 --- a/server/src/main/java/de/ozgcloud/antragsraum/security/JwtTokenFilter.java +++ b/server/src/main/java/de/ozgcloud/antragsraum/security/JwtTokenFilter.java @@ -20,16 +20,15 @@ package de.ozgcloud.antragsraum.security; +import java.io.IOException; +import java.util.NoSuchElementException; +import java.util.Optional; + import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import java.io.IOException; -import java.util.NoSuchElementException; -import java.util.Optional; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import lombok.extern.log4j.Log4j2; + import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; @@ -41,53 +40,56 @@ import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.filter.OncePerRequestFilter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.log4j.Log4j2; + @Log4j2 @RequiredArgsConstructor @Component public class JwtTokenFilter extends OncePerRequestFilter { - public static final String TOKEN_HEADER = "Authorization"; - public static final String TOKEN_PREFIX = "Bearer "; + public static final String TOKEN_HEADER = "Authorization"; + public static final String TOKEN_PREFIX = "Bearer "; - private final UserDetailsService userDetailsService; - private final JwtTokenVerifier jwtTokenVerifier; + private final UserDetailsService userDetailsService; + private final JwtTokenVerifier jwtTokenVerifier; - @Override - protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, - @NonNull FilterChain chain) throws ServletException, IOException { - try { - getJwtFromRequest(request) - .flatMap(jwtTokenVerifier::validate).flatMap(jwsParser -> getJwtFromRequest(request) - .flatMap(token -> jwtTokenVerifier.getJws(jwsParser, token))) - .ifPresent(jws -> setUserToSecurityContext( - createAuthentication(request, jws.getPayload().getSubject(), getJwtFromRequest(request)))); - } catch (NoSuchElementException | IllegalStateException e) { - LOG.debug("Cannot set user authentication", e); - throw new AccessDeniedException("User not found or invalid token"); - } + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, + @NonNull FilterChain chain) throws ServletException, IOException { + try { + getJwtFromRequest(request) + .flatMap(jwtTokenVerifier::validate).flatMap(jwsParser -> getJwtFromRequest(request) + .flatMap(token -> jwtTokenVerifier.getJws(jwsParser, token))) + .ifPresent(jws -> setUserToSecurityContext( + createAuthentication(request, jws.getPayload().getSubject(), getJwtFromRequest(request)))); + } catch (NoSuchElementException | IllegalStateException e) { + LOG.debug("Cannot set user authentication", e); + throw new AccessDeniedException("User not found or invalid token"); + } - chain.doFilter(request, response); - } + chain.doFilter(request, response); + } - Authentication createAuthentication(@NonNull HttpServletRequest request, String username, - Optional<String> tokenOptional) { - UserDetails userDetails = userDetailsService.loadUserByUsername(username); - String token = tokenOptional.orElse(""); - UsernamePasswordAuthenticationToken authentication = - new UsernamePasswordAuthenticationToken(userDetails, token, userDetails.getAuthorities()); - authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + Authentication createAuthentication(@NonNull HttpServletRequest request, String username, Optional<String> tokenOptional) { + UserDetails userDetails = userDetailsService.loadUserByUsername(username); + String token = tokenOptional.orElse(""); + UsernamePasswordAuthenticationToken authentication = + new UsernamePasswordAuthenticationToken(userDetails, token, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); - return authentication; - } + return authentication; + } - void setUserToSecurityContext(Authentication authentication) { - SecurityContextHolder.getContext().setAuthentication(authentication); - } + void setUserToSecurityContext(Authentication authentication) { + SecurityContextHolder.getContext().setAuthentication(authentication); + } - Optional<String> getJwtFromRequest(HttpServletRequest request) { - String tokenHeader = request.getHeader(TOKEN_HEADER); - if (StringUtils.hasText(tokenHeader) && tokenHeader.startsWith(TOKEN_PREFIX)) { - return Optional.of(tokenHeader.replace(TOKEN_PREFIX, "").trim()); - } - return Optional.empty(); - } + Optional<String> getJwtFromRequest(HttpServletRequest request) { + String tokenHeader = request.getHeader(TOKEN_HEADER); + if (StringUtils.hasText(tokenHeader) && tokenHeader.startsWith(TOKEN_PREFIX)) { + return Optional.of(tokenHeader.replace(TOKEN_PREFIX, "").trim()); + } + return Optional.empty(); + } } diff --git a/server/src/main/resources/application-local.yml b/server/src/main/resources/application-local.yml index dd87a88dc3cbdae321115bfcc17871c9faa65746..7faf55c9133054c16a6ef664f0a9b94160aa1237 100644 --- a/server/src/main/resources/application-local.yml +++ b/server/src/main/resources/application-local.yml @@ -36,16 +36,16 @@ spring: relyingparty: registration: bayernid: - entity-id: http://mock-idp + entity-id: https://sso.dev.by.ozg-cloud.de/realms/by-antragsraum-idp signing: credentials: - - private-key-location: "classpath:/mujina-local.key" - certificate-location: "classpath:/mujina-local.crt" + - private-key-location: "classpath:/bayernid-dev-sign.key" + certificate-location: "classpath:/bayernid-dev-sign.crt" decryption: credentials: - - private-key-location: "classpath:/mujina-local.key" - certificate-location: "classpath:/mujina-local.crt" + - private-key-location: "classpath:/bayernid-dev-sign.key" + certificate-location: "classpath:/bayernid-dev-sign.crt" assertingparty: singlesignon: sign-request: true - metadata-uri: "http://localhost:8088/metadata" \ No newline at end of file + metadata-uri: "classpath:/metadata/keycloak-saml-metadata.xml" \ No newline at end of file diff --git a/server/src/main/resources/metadata/okta-idp.xml b/server/src/main/resources/metadata/okta-idp.xml deleted file mode 100644 index 2b294cce4fa05fb4e1b32e86fdd9180ba0728ad9..0000000000000000000000000000000000000000 --- a/server/src/main/resources/metadata/okta-idp.xml +++ /dev/null @@ -1,41 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<md:EntityDescriptor entityID="http://www.okta.com/exkeeichkhdEpMLcK5d7" - xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"> - <md:IDPSSODescriptor WantAuthnRequestsSigned="false" - protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol"> - <md:KeyDescriptor use="signing"> - <ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"> - <ds:X509Data> - <ds:X509Certificate>MIIDqDCCApCgAwIBAgIGAYz3208uMA0GCSqGSIb3DQEBCwUAMIGUMQswCQYDVQQGEwJVUzETMBEG - A1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsGA1UECgwET2t0YTEU - MBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi0wNDg0OTQ4NzEcMBoGCSqGSIb3DQEJ - ARYNaW5mb0Bva3RhLmNvbTAeFw0yNDAxMTEwOTI4NTRaFw0zNDAxMTEwOTI5NTRaMIGUMQswCQYD - VQQGEwJVUzETMBEGA1UECAwKQ2FsaWZvcm5pYTEWMBQGA1UEBwwNU2FuIEZyYW5jaXNjbzENMAsG - A1UECgwET2t0YTEUMBIGA1UECwwLU1NPUHJvdmlkZXIxFTATBgNVBAMMDGRldi0wNDg0OTQ4NzEc - MBoGCSqGSIb3DQEJARYNaW5mb0Bva3RhLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC - ggEBAMYySzBZX6VmleW0+259bgvQdfruEGwbS1Jg0TKmBTO4gIhBZzyaQY5kq1EkuVrrg3TMnF0G - Gws2LjF99t4L93d4wBX0WrrehEttvPgiz3qMcjlgqbynM1zTw9w/TbO61lFf6kcwaSu6wbTFeMwB - l5WYBRPg/LcUU0Qddw3NKuVXk0BX5iPCUP+nWczmkrpByfYPYFaMUcFxrY0NnMCQ6/VcrZfPN2yu - ZX+fAV2Bx56o4QD73PCB6XXlOLo6p3PlqGpIMf/NDdBB3ACtIbQuc+l7Bm8dvjZOyV665I6jq9ZP - MOX8Qo8SKIZz2Hh9AvHSwIKVW4aKTKxQ0LQ+I78Xr9UCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA - Y+A+hCMIfjICOzdu6cD8AOrm2PIo1zcMR+mmUKt1l+Gw8gku//uIRc55N+Gga6/rYl6Og2fe+uip - qzQMjXTtmizI+unukVZeJDkfy8sBwNvuikHs2no0HpS7uqgb1gvVKhvyelmD6UsW0mef0b+QRJJU - rNn3LBGYYw4sB3aKTQlmhPE2M7dKbV3N6dxjPneZf4lNSEhsXFBnAEcxczqy37SFCj5JcDInmwwF - 2gC0dVqEL7S+JFy6xAyDfn4iBoO+DqgMS6OOAN6kvvR5PB+RYnTqT1lWfcqkeHR9fMfpEGPlxgJ7 - MrgVs3cAR5kJKpSbdNyoBWswRQlm+jX2wHJK0Q== - </ds:X509Certificate> - </ds:X509Data> - </ds:KeyInfo> - </md:KeyDescriptor> - <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" - Location="https://dev-04849487.okta.com/app/dev-04849487_antragsraumsaml_1/exkeeichkhdEpMLcK5d7/slo/saml"/> - <md:SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - Location="https://dev-04849487.okta.com/app/dev-04849487_antragsraumsaml_1/exkeeichkhdEpMLcK5d7/slo/saml"/> - <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified</md:NameIDFormat> - <md:NameIDFormat>urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress</md:NameIDFormat> - <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" - Location="https://dev-04849487.okta.com/app/dev-04849487_antragsraumsaml_1/exkeeichkhdEpMLcK5d7/sso/saml"/> - <md:SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" - Location="https://dev-04849487.okta.com/app/dev-04849487_antragsraumsaml_1/exkeeichkhdEpMLcK5d7/sso/saml"/> - </md:IDPSSODescriptor> -</md:EntityDescriptor> \ No newline at end of file diff --git a/server/src/test/helm/deployment_env_test.yaml b/server/src/test/helm/deployment_env_test.yaml index 9a9a7c7d5df4fe0988dbce806ffc71792e8cfb3f..d53e5e80f52e9a26ab9970466cab569d41f0d1af 100644 --- a/server/src/test/helm/deployment_env_test.yaml +++ b/server/src/test/helm/deployment_env_test.yaml @@ -192,4 +192,34 @@ tests: path: spec.template.spec.containers[0].env content: name: SPRING_SECURITY_SAML2_RELYINGPARTY_REGISTRATION_BAYERNID_ENTITY-ID - value: https://sso.dev.de/realms/by-antragsraum-idp \ No newline at end of file + value: https://sso.dev.de/realms/by-antragsraum-idp + + - it: should contain default spring servlet envs + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: SPRING_SERVLET_MULTIPART_MAX-FILE-SIZE + value: 30MB + - contains: + path: spec.template.spec.containers[0].env + content: + name: SPRING_SERVLET_MULTIPART_MAX-REQUEST-SIZE + value: 50MB + - it: should set spring servlet envs + set: + antragsraum: + servlet: + maxRequestSize: 40MB + maxFileSize: 35MB + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: SPRING_SERVLET_MULTIPART_MAX-FILE-SIZE + value: 35MB + - contains: + path: spec.template.spec.containers[0].env + content: + name: SPRING_SERVLET_MULTIPART_MAX-REQUEST-SIZE + value: 40MB \ No newline at end of file diff --git a/server/src/test/java/de/ozgcloud/antragsraum/SecurityConfigurationTest.java b/server/src/test/java/de/ozgcloud/antragsraum/SecurityConfigurationTest.java index 8ba32440719497f65c3c470778c4bb8898fd553d..cd4b8c89ddaed198489fb792692d79632c81731b 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/SecurityConfigurationTest.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/SecurityConfigurationTest.java @@ -31,6 +31,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository; import org.springframework.security.saml2.provider.service.web.authentication.Saml2AuthenticationRequestResolver; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import de.ozgcloud.antragsraum.security.AntragsraumLogoutSuccessHandler; @@ -40,6 +41,7 @@ import de.ozgcloud.antragsraum.security.BayernIdSaml2Extension; import de.ozgcloud.antragsraum.security.InMemoryUserDetailService; import de.ozgcloud.antragsraum.security.JwtTokenFilter; import de.ozgcloud.antragsraum.security.SamlUrlAuthenticationSuccessHandler; +import de.ozgcloud.antragsraum.security.SecurityProvider; @ExtendWith(MockitoExtension.class) class SecurityConfigurationTest { @@ -65,6 +67,20 @@ class SecurityConfigurationTest { securityConfiguration = new SecurityConfiguration(bayernIdSaml2Extension, properties, userDetailsService, tokenFilter, corsProperties); } + @Test + void shouldCreateSecurityProvider() { + SecurityProvider securityProvider = securityConfiguration.securityProvider(); + + assertThat(securityProvider).isNotNull(); + } + + @Test + void shouldCreateLogoutHandler() { + SecurityContextLogoutHandler logoutHandler = securityConfiguration.logoutHandler(); + + assertThat(logoutHandler).isNotNull(); + } + @Nested class TestHttpSecurity { private HttpSecurity httpSecurity; @@ -77,6 +93,7 @@ class SecurityConfigurationTest { when(httpSecurity.saml2Logout(any())).thenReturn(httpSecurity); when(httpSecurity.addFilterBefore(any(), any())).thenReturn(httpSecurity); when(httpSecurity.csrf(any())).thenReturn(httpSecurity); + when(httpSecurity.headers(any())).thenReturn(httpSecurity); } @Test @@ -99,6 +116,13 @@ class SecurityConfigurationTest { verify(httpSecurity).saml2Logout(any()); } + + @Test + void shouldSetupHeaders() throws Exception { + securityConfiguration.filterChain(httpSecurity); + + verify(httpSecurity, times(2)).headers(any()); + } } @Nested @@ -123,8 +147,10 @@ class SecurityConfigurationTest { void setUp() throws Exception { when(security.authorizeHttpRequests(any())).thenReturn(security); when(security.addFilterBefore(any(), any())).thenReturn(security); + when(security.headers(any())).thenReturn(security); when(security.saml2Login(any())).thenReturn(security); when(security.saml2Logout(any())).thenReturn(security); + when(security.csrf(any())).thenReturn(security); } @Test diff --git a/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileControllerTestConfiguration.java b/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileControllerTestConfiguration.java index a705fab72494e0c4296199d064f862e32ce26fd0..006f96223eda9846b73a87120795f8541414d0c1 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileControllerTestConfiguration.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileControllerTestConfiguration.java @@ -36,10 +36,10 @@ import de.ozgcloud.antragsraum.WebConfiguration; @EnableAsync @ComponentScan(value = { "de.ozgcloud.antragsraum.attachments", "de.ozgcloud.antragsraum.common" }, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, - classes = FileRemoteServiceTestConfiguration.class)) + classes = { FileRemoteServiceTestConfiguration.class, FileRestClientTestConfiguration.class })) @Configuration public class FileControllerTestConfiguration { - + RestClient.Builder restClientBuilder() { return RestClient.builder().baseUrl("http://localhost:8080"); } diff --git a/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileRemoteServiceTestConfiguration.java b/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileRemoteServiceTestConfiguration.java index 196a35a9000ff34802f8fd26c87c437fb4ca0d73..532bc9f603d52860ceab5392bd79cb7ec5b74c34 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileRemoteServiceTestConfiguration.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileRemoteServiceTestConfiguration.java @@ -34,7 +34,7 @@ import de.ozgcloud.antragsraum.events.NachrichtEventService; @ComponentScan(value = { "de.ozgcloud.antragsraum.attachments", "de.ozgcloud.antragsraum.common" }, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, - classes = { FileControllerTestConfiguration.class, FileController.class })) + classes = { FileControllerTestConfiguration.class, FileController.class, FileRestClientTestConfiguration.class })) @Configuration public class FileRemoteServiceTestConfiguration { @Bean diff --git a/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileRestClientTest.java b/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileRestClientTest.java new file mode 100644 index 0000000000000000000000000000000000000000..45af2c4e9e4efcae7fded3f6becc5cfe972ca858 --- /dev/null +++ b/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileRestClientTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2024. Das Land Schleswig-Holstein vertreten durch den Ministerpräsidenten + * des Landes Schleswig-Holstein Staatskanzlei Abteilung Digitalisierung und zentrales IT-Management der Landesregierung. + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ + +package de.ozgcloud.antragsraum.attachments; + +import static de.ozgcloud.antragsraum.attachments.FileRestClient.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; +import static org.springframework.test.web.client.response.MockRestResponseCreators.*; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.boot.test.web.client.MockServerRestClientCustomizer; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestClient; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.ozgcloud.antragsraum.common.TechnicalException; +import de.ozgcloud.antragsraum.proxy.AntragraumproxyGrpcFindFilesResponse; + +@SpringJUnitConfig(classes = { FileRestClientTestConfiguration.class }) +@RestClientTest(FileRestClient.class) +class FileRestClientTest { + final static String ADDRESS = "http://localhost:8382"; + public static final String FILE_ID = "test"; + private FileRestClient fileRestClient; + + @Autowired + private RestClient.Builder restClientBuilder; + + @Autowired + private MockServerRestClientCustomizer customizer; + + private MockRestServiceServer server; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setupRestClient() { + customizer.customize(restClientBuilder); + server = customizer.getServer(restClientBuilder); + fileRestClient = new FileRestClient(restClientBuilder.build()); + } + + @Nested + class TestLoadingFileMetadata { + private final String fileFileUri = ADDRESS + FIND_FILE_METADATA_API.replace("{fileId}", FILE_ID); + + @BeforeEach + void setup() throws JsonProcessingException { + server.expect(requestTo(fileFileUri)) + .andRespond(withSuccess(objectMapper.writeValueAsString(createFindFileMetadataResponse()), MediaType.APPLICATION_JSON)); + } + + private AntragraumproxyGrpcFindFilesResponse createFindFileMetadataResponse() { + return AntragraumproxyGrpcFindFilesResponseTestFactory.create(); + } + + @Test + void shouldFindFileMetadata() { + var fileMetadata = fileRestClient.findBinaryFilesMetaData(FILE_ID, ADDRESS + FIND_FILE_METADATA_API); + + assertThat(fileMetadata).isNotNull(); + } + } + + @Nested + class TestUploadingFile { + + @Test + void shouldUploadFile() throws JsonProcessingException { + server.expect(requestTo(ADDRESS + UPLOAD_FILE_API)) + .andRespond(withSuccess(objectMapper.writeValueAsString(UUID.randomUUID().toString()), MediaType.APPLICATION_JSON)); + + CompletableFuture<String> fileId = new CompletableFuture<>(); + fileRestClient.uploadFile(OzgUploadFileTestFactory.create(), ADDRESS + UPLOAD_FILE_API, fileId); + + assertThat(fileId).isCompleted(); + } + + @Test + void shouldHandleEmptyUploadFile() { + server.expect(requestTo(ADDRESS + UPLOAD_FILE_API)) + .andRespond(withSuccess("", MediaType.APPLICATION_JSON)); + + CompletableFuture<String> fileId = new CompletableFuture<>(); + fileRestClient.uploadFile(OzgUploadFileTestFactory.create(), ADDRESS + UPLOAD_FILE_API, fileId); + + assertThat(fileId).withFailMessage(NO_FILE_ID_RECEIVED_MESSAGE).isCompletedExceptionally(); + } + } + + @Nested + class TestDownloadFileContent { + private final String fileContentUri = ADDRESS + GET_FILE_CONTENT_API.replace("{fileId}", FILE_ID); + + @BeforeEach + void setup() throws IOException { + server.expect(requestTo(fileContentUri)) + .andRespond(withSuccess(OzgUploadFileTestFactory.MULTIPART_FILE.getBytes(), MediaType.APPLICATION_OCTET_STREAM)); + } + + @Test + void shouldDownloadFileContent() throws IOException { + var out = new ByteArrayOutputStream(); + fileRestClient.downloadFileContent(FILE_ID, out, ADDRESS + GET_FILE_CONTENT_API); + var res = out.toByteArray(); + + assertThat(res).isEqualTo(OzgUploadFileTestFactory.MULTIPART_FILE.getBytes()); + } + + @Test + void shouldDownloadFileContentIoException() { + var out = mock(ByteArrayOutputStream.class); + doThrow(IOException.class).when(out).write(any(byte[].class), anyInt(), anyInt()); + + assertThatExceptionOfType(TechnicalException.class).isThrownBy( + () -> fileRestClient.downloadFileContent(FILE_ID, out, ADDRESS + GET_FILE_CONTENT_API)); + } + } +} \ No newline at end of file diff --git a/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileRestClientTestConfiguration.java b/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileRestClientTestConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..73d964c9b5375a27e8ba58413ea843d2324bb41a --- /dev/null +++ b/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileRestClientTestConfiguration.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2023-2024. Das Land Schleswig-Holstein vertreten durch den Ministerpräsidenten + * des Landes Schleswig-Holstein Staatskanzlei Abteilung Digitalisierung und zentrales IT-Management der Landesregierung. + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.antragsraum.attachments; + +import jakarta.validation.Validator; + +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.client.RestClient; + +import de.ozgcloud.antragsraum.WebConfiguration; +import de.ozgcloud.antragsraum.events.NachrichtEventService; + +@EnableAsync +@ComponentScan(value = { "de.ozgcloud.antragsraum.attachments", "de.ozgcloud.antragsraum.common" }, + excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, + classes = { FileControllerTestConfiguration.class, FileController.class, FileRemoteServiceTestConfiguration.class })) +@Configuration +public class FileRestClientTestConfiguration { + @MockBean + FileService fileService; + + @MockBean + NachrichtEventService nachrichtEventService; + + @Bean + public RestClient.Builder restClientBuilder() { + return RestClient.builder().baseUrl(FileRestClientTest.ADDRESS); + } + + @Bean(name = WebConfiguration.NACHRICHT_REST_CLIENT) + RestClient fileRestClient() { + return restClientBuilder().build(); + } + + @Bean + public Validator defaultValidator() { + return new LocalValidatorFactoryBean(); + } + + @MockBean + VirusScannerClient virusScannerClient; + +} diff --git a/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileValidatorTest.java b/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileValidatorTest.java index 00a40704d540c5ead0256ed8ca0cd0d692c266ca..97ef3384b172d94a275d632bc07c5c5e15c3f4d1 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileValidatorTest.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/attachments/FileValidatorTest.java @@ -70,13 +70,10 @@ class FileValidatorTest { @Nested class TestFileContentTypeIsValid { - @BeforeEach - void init() { - when(fileProperties.getBlockedContentTypes()).thenReturn(new String[] { INVALID_CONTENT_TYPE }); - } - @Test void shouldBeValid() { + when(fileProperties.getBlockedContentTypes()).thenReturn(new String[] { INVALID_CONTENT_TYPE }); + var result = validator.fileContentTypeIsValid(CONTENT_TYPE); assertThat(result).isTrue(); @@ -84,10 +81,19 @@ class FileValidatorTest { @Test void shouldBeInvalid() { + when(fileProperties.getBlockedContentTypes()).thenReturn(new String[] { INVALID_CONTENT_TYPE }); + var result = validator.fileContentTypeIsValid(INVALID_CONTENT_TYPE); assertThat(result).isFalse(); } + + @Test + void shouldBeInvalidBecauseNotSet() { + var result = validator.fileContentTypeIsValid(null); + + assertThat(result).isFalse(); + } } @Nested diff --git a/server/src/test/java/de/ozgcloud/antragsraum/command/CommandControllerTestConfiguration.java b/server/src/test/java/de/ozgcloud/antragsraum/command/CommandControllerTestConfiguration.java index b02268cf28a716e0093fced02455c87577014b77..a2d0064a158a0e4f45f9e96485a0376792bd06c8 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/command/CommandControllerTestConfiguration.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/command/CommandControllerTestConfiguration.java @@ -28,12 +28,15 @@ import static org.mockito.Mockito.*; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.web.servlet.config.annotation.EnableWebMvc; @EnableWebMvc @EnableAsync -@ComponentScan(value = { "de.ozgcloud.antragsraum.command", "de.ozgcloud.antragsraum.common" }) +@ComponentScan(value = { "de.ozgcloud.antragsraum.command", "de.ozgcloud.antragsraum.common" }, + excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, + classes = { CommandRestClientTestConfiguration.class, CommandRemoteServiceTestConfiguration.class })) @Configuration public class CommandControllerTestConfiguration { @Bean diff --git a/server/src/test/java/de/ozgcloud/antragsraum/command/CommandRemoteServiceTestConfiguration.java b/server/src/test/java/de/ozgcloud/antragsraum/command/CommandRemoteServiceTestConfiguration.java index ef94b3b59946692d5a9b21fe28cd71180fef2f86..7287f61a8b30a51d20ffb55778b98e0b3d8bd187 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/command/CommandRemoteServiceTestConfiguration.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/command/CommandRemoteServiceTestConfiguration.java @@ -32,7 +32,7 @@ import de.ozgcloud.antragsraum.events.NachrichtEventService; @EnableAsync @ComponentScan(value = { "de.ozgcloud.antragsraum.command", "de.ozgcloud.antragsraum.common" }, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, - classes = { CommandController.class })) + classes = { CommandController.class, CommandRestClientTestConfiguration.class })) @Configuration public class CommandRemoteServiceTestConfiguration { diff --git a/server/src/test/java/de/ozgcloud/antragsraum/command/CommandRestClientTest.java b/server/src/test/java/de/ozgcloud/antragsraum/command/CommandRestClientTest.java index 507d97e308a44485fb0e6bc9643350a369f760e3..0a99eff18ed770c6f34b8d255f3542aede8d704d 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/command/CommandRestClientTest.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/command/CommandRestClientTest.java @@ -21,33 +21,59 @@ */ package de.ozgcloud.antragsraum.command; -import static org.mockito.Mockito.*; +import static de.ozgcloud.antragsraum.command.CommandRestClient.*; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.*; +import static org.springframework.test.web.client.response.MockRestResponseCreators.*; import java.util.UUID; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.InjectMocks; -import org.mockito.Spy; -import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.client.RestClientTest; +import org.springframework.boot.test.web.client.MockServerRestClientCustomizer; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestClient; -import de.ozgcloud.antragsraum.proxy.AntragraumproxyGrpcCommand; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; -@ExtendWith(MockitoExtension.class) +@SpringJUnitConfig(classes = { CommandRestClientTestConfiguration.class }) +@RestClientTest(CommandRestClient.class) class CommandRestClientTest { - private final static String SERVICE_ADDRESS = "static://localhost:9090"; + final static String ADDRESS = "http://localhost:8382"; private static final String COMMAND_ID = UUID.randomUUID().toString(); - @Spy - @InjectMocks - private CommandRestClient restClient; + private CommandRestClient commandRestClient; + + @Autowired + private RestClient.Builder restClientBuilder; + + @Autowired + private MockServerRestClientCustomizer customizer; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + private final String commandApi = ADDRESS + COMMAND_URI.replace("{commandId}", COMMAND_ID); + + @BeforeEach + void setup() throws JsonProcessingException { + customizer.customize(restClientBuilder); + MockRestServiceServer server = customizer.getServer(restClientBuilder); + commandRestClient = new CommandRestClient(restClientBuilder.build()); + + server.expect(requestTo(commandApi)) + .andRespond( + withSuccess(objectMapper.writeValueAsString(AntragraumproxyGrpcCommandTestFactory.createCommand()), MediaType.APPLICATION_JSON)); + } @Test void shouldCallGetCommand() { - doReturn(mock(AntragraumproxyGrpcCommand.class)).when(restClient).getCommand(any(), any()); - - restClient.getCommand(COMMAND_ID, SERVICE_ADDRESS); + var command = commandRestClient.getCommand(COMMAND_ID, ADDRESS + COMMAND_URI); - verify(restClient).getCommand(any(), any()); + assertThat(command.getId()).isEqualTo(AntragraumproxyGrpcCommandTestFactory.ID); } } \ No newline at end of file diff --git a/server/src/test/java/de/ozgcloud/antragsraum/command/CommandRestClientTestConfiguration.java b/server/src/test/java/de/ozgcloud/antragsraum/command/CommandRestClientTestConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..ae11e9f710cdc32b336f9e41e8d98c508745c557 --- /dev/null +++ b/server/src/test/java/de/ozgcloud/antragsraum/command/CommandRestClientTestConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2023-2024. Das Land Schleswig-Holstein vertreten durch den Ministerpräsidenten + * des Landes Schleswig-Holstein Staatskanzlei Abteilung Digitalisierung und zentrales IT-Management der Landesregierung. + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.antragsraum.command; + +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.web.client.RestClient; + +import de.ozgcloud.antragsraum.WebConfiguration; + +@EnableAsync +@ComponentScan(value = { "de.ozgcloud.antragsraum.command", "de.ozgcloud.antragsraum.common" }, + excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, + classes = { CommandControllerTestConfiguration.class, CommandController.class, CommandRemoteServiceTestConfiguration.class })) +@Configuration +public class CommandRestClientTestConfiguration { + + @Bean + public RestClient.Builder restClientBuilder() { + return RestClient.builder().baseUrl(CommandRestClientTest.ADDRESS); + } + + @Bean(name = WebConfiguration.NACHRICHT_REST_CLIENT) + RestClient commandRestClient() { + return restClientBuilder().build(); + } + + @MockBean + CommandService commandService; + +} diff --git a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtMapperTest.java b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtMapperTest.java index a8210912f787379296705cc1098024a68b8e9805..882721adbbcb05259680c8735c5f092b4624cec0 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtMapperTest.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtMapperTest.java @@ -48,6 +48,15 @@ class NachrichtMapperTest { @InjectMocks private NachrichtMapper mapper; + @Test + void shouldMapEmptyAttachmentFields() { + var rueckfrage = AntragraumproxyGrpcRueckfrageTestFactory.createRueckfrage(); + rueckfrage.setAttachmentFileId(null); + var nachricht = mapper.fromRestRueckfrage(rueckfrage, NachrichtTestFactory.NACHRICHT_EVENT_ID); + + assertThat(nachricht.attachments()).isEmpty(); + } + @Nested class TestFromRestRueckfrage { private final AntragraumproxyGrpcRueckfrage rueckfrage = AntragraumproxyGrpcRueckfrageTestFactory.createRueckfrage(); @@ -120,6 +129,15 @@ class NachrichtMapperTest { assertThat(nachricht.answeredAt()).isZero(); } + @Test + void shouldMapEmptyAnsweredAt() { + var rueckfrage = AntragraumproxyGrpcRueckfrageTestFactory.createRueckfrage(); + rueckfrage.setAnsweredAt(""); + var nachricht = mapper.fromRestRueckfrage(rueckfrage, NachrichtTestFactory.NACHRICHT_EVENT_ID); + + assertThat(nachricht.answeredAt()).isZero(); + } + @Test void shouldMapAttachments() { var nachricht = mapper.fromRestRueckfrage(AntragraumproxyGrpcRueckfrageTestFactory.createRueckfrage(), diff --git a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtServiceTestConfiguration.java b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtServiceTestConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..69b632c38601e06fb675ec4a35cd2d301b1d375a --- /dev/null +++ b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtServiceTestConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023-2024. + * Das Land Schleswig-Holstein vertreten durch den Ministerpräsidenten des Landes Schleswig-Holstein Staatskanzlei Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.antragsraum.nachricht; + +import jakarta.validation.Validator; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.FilterType; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; +import org.springframework.web.client.RestClient; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import de.ozgcloud.antragsraum.WebConfiguration; + +@EnableWebMvc +@EnableAsync +@ComponentScan(value = { "de.ozgcloud.antragsraum.nachricht", "de.ozgcloud.antragsraum.common" }, + excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, + classes = { NachrichtenRemoteServiceTestConfiguration.class, NachrichtenRestClientTestConfiguration.class, + NachrichtenControllerTestConfiguration.class })) +@Configuration +public class NachrichtServiceTestConfiguration { + @Bean(name = WebConfiguration.NACHRICHT_REST_CLIENT) + public RestClient restClient() { + return RestClient.builder() + .baseUrl("http//localhost:8080") + .build(); + } + + @Bean + public Validator defaultValidator() { + return new LocalValidatorFactoryBean(); + } +} diff --git a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtTest.java b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtTest.java new file mode 100644 index 0000000000000000000000000000000000000000..200b613a84fa15bcc10bd25b3685b304af9b3fc2 --- /dev/null +++ b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024. Das Land Schleswig-Holstein vertreten durch den Ministerpräsidenten + * des Landes Schleswig-Holstein Staatskanzlei Abteilung Digitalisierung und zentrales IT-Management der Landesregierung. + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ + +package de.ozgcloud.antragsraum.nachricht; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class NachrichtTest { + private final Nachricht nachricht = NachrichtTestFactory.createNachricht(); + + @Test + void shouldNotPrintMessageText() { + String nachrichtString = nachricht.toString(); + + assertThat(nachrichtString).doesNotContain(NachrichtTestFactory.TEXT); + } +} \ No newline at end of file diff --git a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenControllerITCase.java b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenControllerITCase.java index fdd47a4fb0026427804cb10fdf39411f07936f43..c50db1e212f58992e3f1ebed9230a78279349a92 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenControllerITCase.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenControllerITCase.java @@ -29,6 +29,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. import java.nio.charset.Charset; import java.util.List; +import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -209,14 +210,6 @@ public class NachrichtenControllerITCase { .andExpect(jsonPath("$.nachrichtEventId").value(CommandReferenceTestFactory.NACHRICHT_EVENT_ID)); } - @Test - @WithMockUser - void shouldNotSendNachrichtBlankId() throws Exception { - var invalidMsg = ReplyNachrichtTestFactory.createBuilder().id("").build(); - - performPutRequest(mapper.writeValueAsString(invalidMsg)).andExpect(status().isBadRequest()); - } - @Test @WithMockUser void shouldNotSendNachrichtWithoutAddress() throws Exception { @@ -233,6 +226,49 @@ public class NachrichtenControllerITCase { performPutRequest(mapper.writeValueAsString(msg)).andExpect(status().isInternalServerError()); } + @Nested + class TestSendingInvalidReply { + @Test + @WithMockUser + void shouldNotSendNachrichtBlankId() throws Exception { + var invalidMsg = ReplyNachrichtTestFactory.createBuilder().id("").build(); + + performPutRequest(mapper.writeValueAsString(invalidMsg)).andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser + void shouldHandleInvalidId() throws Exception { + var invalidMsg = msg.toBuilder().id("id_that_is_too_long_123456789").build(); + + performPutRequest(mapper.writeValueAsString(invalidMsg)).andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser + void shouldHandleInvalidNachrichtEventId() throws Exception { + var invalidMsg = msg.toBuilder().nachrichtEventId("id_that_is_too_long_123456789").build(); + + performPutRequest(mapper.writeValueAsString(invalidMsg)).andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser + void shouldHandleInvalidPostfachId() throws Exception { + var invalidMsg = msg.toBuilder().id("postfach_id_that_is_too_long_123456789_123456789").build(); + + performPutRequest(mapper.writeValueAsString(invalidMsg)).andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser + void shouldHandleInvalidMessage() throws Exception { + var invalidMsg = msg.toBuilder().message(RandomStringUtils.random(500001, true, false)).build(); + + performPutRequest(mapper.writeValueAsString(invalidMsg)).andExpect(status().isBadRequest()); + } + } + ResultActions performPutRequest(String body) throws Exception { return mockMvc.perform( put(NachrichtenController.PATH + "/antwort") diff --git a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenControllerTestConfiguration.java b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenControllerTestConfiguration.java index 5d5d5eff162baa5b531a38f7c90bdc1d8dba7f7b..0b87eca4ef6aa7b48974d7976d95ac82c989cdf3 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenControllerTestConfiguration.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenControllerTestConfiguration.java @@ -35,7 +35,8 @@ import de.ozgcloud.antragsraum.WebConfiguration; @EnableAsync @ComponentScan(value = { "de.ozgcloud.antragsraum.nachricht", "de.ozgcloud.antragsraum.common" }, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, - classes = { NachrichtenRemoteServiceTestConfiguration.class, NachrichtenRestClientTestConfiguration.class })) + classes = { NachrichtenRemoteServiceTestConfiguration.class, NachrichtenRestClientTestConfiguration.class, + NachrichtServiceTestConfiguration.class })) @Configuration public class NachrichtenControllerTestConfiguration { @Bean(name = WebConfiguration.NACHRICHT_REST_CLIENT) diff --git a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRemoteServiceTest.java b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRemoteServiceTest.java index 627f0e0cad1b24a1802cd737fd2ab32a59cec471..d866a13274a88ad3014c042adaee4d1a65d5cf25 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRemoteServiceTest.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRemoteServiceTest.java @@ -52,6 +52,7 @@ import de.ozgcloud.antragsraum.proxy.AntragraumproxyGrpcRueckfrageAnswer; import de.ozgcloud.antragsraum.proxy.AntragraumproxyGrpcRueckfrageHead; import de.ozgcloud.antragsraum.proxy.AntragraumproxyGrpcSendRueckfrageAnswerResponse; import de.ozgcloud.antragsraum.security.UserTestFactory; +import io.grpc.Status; import io.grpc.StatusRuntimeException; @ExtendWith(MockitoExtension.class) @@ -107,6 +108,16 @@ public class NachrichtenRemoteServiceTest { verify(nachrichtenRestClient).getFindRueckfragen(anyString(), anyString(), anyString()); } + + @Test + void shouldHandleNullResult() { + when(nachrichtenRestClient.getFindRueckfragen(anyString(), anyString(), anyString())).thenReturn( + new AntragraumproxyGrpcFindRueckfragenResponse()); + + nachrichtenRemoteService.findRueckfrageHeads(nachrichtEvent); + + verify(nachrichtenRestClient).getFindRueckfragen(anyString(), anyString(), anyString()); + } } @Nested @@ -167,6 +178,31 @@ public class NachrichtenRemoteServiceTest { } } + @Nested + class TestGetNachrichtError { + @Test + void shouldHandleNullRueckfrage() { + when(nachrichtenRestClient.getGetRueckfrage(anyString(), anyString(), anyString())).thenReturn( + new AntragraumproxyGrpcGetRueckfrageResponse()); + + nachrichtenRemoteService.getRueckfrage(AntragraumproxyGrpcRueckfrageTestFactory.RUECKFRAGE_ID, + NachrichtEventTestFactory.createNachrichtEvent()); + + verify(nachrichtenRestClient).getGetRueckfrage(anyString(), anyString(), anyString()); + } + + @Test + void shouldHandleError() { + when(nachrichtenRestClient.getGetRueckfrage(anyString(), anyString(), anyString())).thenThrow( + new StatusRuntimeException(Status.CANCELLED)); + + nachrichtenRemoteService.getRueckfrage(AntragraumproxyGrpcRueckfrageTestFactory.RUECKFRAGE_ID, + NachrichtEventTestFactory.createNachrichtEvent()); + + verify(nachrichtenRestClient).getGetRueckfrage(anyString(), anyString(), anyString()); + } + } + @Nested class TestSendAnswer { private final ReplyNachricht reply = ReplyNachrichtTestFactory.create(); diff --git a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRemoteServiceTestConfiguration.java b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRemoteServiceTestConfiguration.java index 3f5b394aad17b5b37d958034f78c2d43f64b8e64..21f2bd81161301c657450d428e0a7523e6822b53 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRemoteServiceTestConfiguration.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRemoteServiceTestConfiguration.java @@ -22,12 +22,15 @@ package de.ozgcloud.antragsraum.nachricht; import static de.ozgcloud.antragsraum.nachricht.AntragraumproxyGrpcRueckfrageTestFactory.*; import static org.mockito.Mockito.*; +import jakarta.validation.Validator; + import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import de.ozgcloud.antragsraum.WebConfiguration; import de.ozgcloud.antragsraum.attachments.FileService; @@ -38,7 +41,8 @@ import de.ozgcloud.antragsraum.proxy.AntragraumproxyGrpcSendRueckfrageAnswerResp @EnableAsync @ComponentScan(value = { "de.ozgcloud.antragsraum.nachricht", "de.ozgcloud.antragsraum.common" }, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, - classes = { NachrichtenControllerTestConfiguration.class, NachrichtenRestClientTestConfiguration.class, NachrichtenRestClient.class })) + classes = { NachrichtenControllerTestConfiguration.class, NachrichtenRestClientTestConfiguration.class, NachrichtenRestClient.class, + NachrichtServiceTestConfiguration.class })) @Configuration public class NachrichtenRemoteServiceTestConfiguration { @MockBean @@ -62,4 +66,9 @@ public class NachrichtenRemoteServiceTestConfiguration { return nachrichtenRestClient; } + @Bean + public Validator defaultValidator() { + return new LocalValidatorFactoryBean(); + } + } diff --git a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRestClientTestConfiguration.java b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRestClientTestConfiguration.java index 1f5bccbc5ff9885e9dca86f407770f36b331a3fb..e39f0412df4f61466aea73e25a78437b5d91396b 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRestClientTestConfiguration.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenRestClientTestConfiguration.java @@ -21,12 +21,15 @@ */ package de.ozgcloud.antragsraum.nachricht; +import jakarta.validation.Validator; + import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.FilterType; import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; import org.springframework.web.client.RestClient; import de.ozgcloud.antragsraum.WebConfiguration; @@ -36,7 +39,8 @@ import de.ozgcloud.antragsraum.events.NachrichtEventService; @EnableAsync @ComponentScan(value = { "de.ozgcloud.antragsraum.nachricht", "de.ozgcloud.antragsraum.common" }, excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, - classes = { NachrichtenControllerTestConfiguration.class, NachrichtenController.class, NachrichtenRemoteServiceTestConfiguration.class })) + classes = { NachrichtenControllerTestConfiguration.class, NachrichtenController.class, NachrichtenRemoteServiceTestConfiguration.class, + NachrichtServiceTestConfiguration.class })) @Configuration public class NachrichtenRestClientTestConfiguration { @MockBean @@ -44,7 +48,7 @@ public class NachrichtenRestClientTestConfiguration { @MockBean NachrichtEventService nachrichtEventService; - + @Bean public RestClient.Builder restClientBuilder() { return RestClient.builder().baseUrl(NachrichtenRestClientTest.ADDRESS); @@ -55,4 +59,9 @@ public class NachrichtenRestClientTestConfiguration { return restClientBuilder().build(); } + @Bean + public Validator defaultValidator() { + return new LocalValidatorFactoryBean(); + } + } diff --git a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenServiceITCase.java b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenServiceITCase.java new file mode 100644 index 0000000000000000000000000000000000000000..ff7af87fd603945c9e75d1b5bdb7db7c0148229d --- /dev/null +++ b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenServiceITCase.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2023-2024. + * Das Land Schleswig-Holstein vertreten durch den Ministerpräsidenten des Landes Schleswig-Holstein Staatskanzlei Abteilung Digitalisierung und zentrales IT-Management der Landesregierung + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ +package de.ozgcloud.antragsraum.nachricht; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import de.ozgcloud.antragsraum.attachments.OzgFile; +import de.ozgcloud.antragsraum.attachments.OzgFileTestFactory; +import de.ozgcloud.antragsraum.command.CommandReferenceTestFactory; +import de.ozgcloud.antragsraum.common.TechnicalException; +import de.ozgcloud.antragsraum.events.NachrichtEventService; +import de.ozgcloud.antragsraum.events.NachrichtEventTestFactory; + +@SpringBootTest +@SpringJUnitConfig(classes = { NachrichtServiceTestConfiguration.class }) +class NachrichtenServiceITCase { + public static final int OVERSIZED_EVENT_ID = 25; + public static final int OVERSIZED_ID = 25; + public static final int OVERSIZED_POSTFACH_ID = UUID.randomUUID().toString().length() + 1; + public static final int OVERSIZED_TEXT = 100_001; + public static final int MAX_ATTACHMENT_COUNT = 21; + @Autowired + private NachrichtenService nachrichtenService; + @MockBean + private NachrichtEventService nachrichtEventService; + @MockBean + private NachrichtenRemoteService nachrichtenRemoteService; + @MockBean + private NachrichtMapper nachrichtMapper; + @MockBean + private NachrichtenController nachrichtenController; + + @Nested + class TestSendNachricht { + @BeforeEach + void init() { + when(nachrichtEventService.getNachrichtEventById(anyString())).thenReturn(NachrichtEventTestFactory.createNachrichtEvent()); + when(nachrichtenRemoteService.sendAnswer(any(ReplyNachricht.class), anyString())).thenReturn(CommandReferenceTestFactory.create()); + } + + @Test + void shouldSendValidReply() { + ReplyNachricht msg = ReplyNachrichtTestFactory.create(); + + nachrichtenService.sendRueckfrageAnswer(msg); + + verify(nachrichtenRemoteService).sendAnswer(any(ReplyNachricht.class), anyString()); + } + + @Nested + class TestSendingInvalidReply { + + @Test + void messageTestTooLong() { + ReplyNachricht msg = ReplyNachrichtTestFactory.createBuilder().message(RandomStringUtils.random(OVERSIZED_TEXT)).build(); + + assertThatExceptionOfType(TechnicalException.class).isThrownBy(() -> nachrichtenService.sendRueckfrageAnswer(msg)) + .withMessageStartingWith("TechnicalException happened! Message: message: Message text too long"); + } + + @Test + void messagePostfachIdTooLong() { + ReplyNachricht msg = ReplyNachrichtTestFactory.createBuilder().postfachId(RandomStringUtils.random(OVERSIZED_POSTFACH_ID)).build(); + + assertThatExceptionOfType(TechnicalException.class).isThrownBy(() -> nachrichtenService.sendRueckfrageAnswer(msg)) + .withMessageStartingWith("TechnicalException happened! Message: postfachId: An id is too long"); + } + + @Test + void messageIdTooLong() { + ReplyNachricht msg = ReplyNachrichtTestFactory.createBuilder().id(RandomStringUtils.random(OVERSIZED_ID)).build(); + + assertThatExceptionOfType(TechnicalException.class).isThrownBy(() -> nachrichtenService.sendRueckfrageAnswer(msg)) + .withMessageStartingWith("TechnicalException happened! Message: id: An id is too long"); + } + + @Test + void messageNachrichtEventIdTooLong() { + ReplyNachricht msg = ReplyNachrichtTestFactory.createBuilder().nachrichtEventId(RandomStringUtils.random(OVERSIZED_EVENT_ID)).build(); + + assertThatExceptionOfType(TechnicalException.class).isThrownBy(() -> nachrichtenService.sendRueckfrageAnswer(msg)) + .withMessageStartingWith("TechnicalException happened! Message: nachrichtEventId: An id id too long"); + } + + @Test + void messageTooManyAttachments() { + List<OzgFile> attachments = new ArrayList<>(MAX_ATTACHMENT_COUNT); + for (int i = 0; i < MAX_ATTACHMENT_COUNT; i++) { + attachments.add(OzgFileTestFactory.create()); + } + + ReplyNachricht msg = ReplyNachrichtTestFactory.createBuilder().attachments(attachments).build(); + + assertThatExceptionOfType(TechnicalException.class).isThrownBy(() -> nachrichtenService.sendRueckfrageAnswer(msg)) + .withMessageStartingWith("TechnicalException happened! Message: attachments: size must be between 0 and 20"); + } + } + } +} \ No newline at end of file diff --git a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenServiceTest.java b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenServiceTest.java index e459afbdec26b3dad332bd966fb8d5813d0e1ebf..536d887582fb096165912c23dc0fa96de27ea71c 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenServiceTest.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/NachrichtenServiceTest.java @@ -28,6 +28,10 @@ import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; import java.util.List; import java.util.Optional; +import java.util.Set; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; @@ -54,6 +58,8 @@ class NachrichtenServiceTest { private NachrichtEventService nachrichtEventService; @Mock private NachrichtenRemoteService nachrichtenRemoteService; + @Mock + private Validator validator; @Nested class TestGetRueckfrageHeadersOfPostfach { @@ -206,4 +212,15 @@ class NachrichtenServiceTest { assertThat(result).isInstanceOf(CommandReference.class); } } + + @Nested + class TestValidation { + @Test + @SuppressWarnings("unchecked") + void shouldNotCallSendOnValidationError() { + when(validator.validate(any(ReplyNachricht.class))).thenReturn(Set.of(mock(ConstraintViolation.class))); + + assertThatException().isThrownBy(() -> nachrichtenService.sendRueckfrageAnswer(ReplyNachrichtTestFactory.create())); + } + } } \ No newline at end of file diff --git a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/ReplyNachrichtTest.java b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/ReplyNachrichtTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ca1cad65796e8514201319ec56e8c7663e9e5f0b --- /dev/null +++ b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/ReplyNachrichtTest.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2024. Das Land Schleswig-Holstein vertreten durch den Ministerpräsidenten + * des Landes Schleswig-Holstein Staatskanzlei Abteilung Digitalisierung und zentrales IT-Management der Landesregierung. + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ + +package de.ozgcloud.antragsraum.nachricht; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class ReplyNachrichtTest { + final ReplyNachricht replyNachricht = ReplyNachrichtTestFactory.create(); + + @Test + void shouldNotPrintMessageText() { + String replyNachrichtText = replyNachricht.toString(); + + assertThat(replyNachrichtText).doesNotContain(ReplyNachrichtTestFactory.TEXT); + } +} \ No newline at end of file diff --git a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/ReplyNachrichtTestFactory.java b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/ReplyNachrichtTestFactory.java index 72225ad0b3f086a94a840ec2fd6ad93d7be30a10..ca44f4f85426e5e143179ad91a164fa8ef4e88aa 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/ReplyNachrichtTestFactory.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/ReplyNachrichtTestFactory.java @@ -21,11 +21,9 @@ */ package de.ozgcloud.antragsraum.nachricht; -import java.util.UUID; - public class ReplyNachrichtTestFactory { - public static final String ID = UUID.randomUUID().toString(); - public static final String NACHRICHT_EVENT_ID = UUID.randomUUID().toString(); + public static final String ID = "60af924b4f1a2560298b4567"; + public static final String NACHRICHT_EVENT_ID = "60af924b4f1a2560298b4567"; public static final String TEXT = "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam"; static ReplyNachricht create() { diff --git a/server/src/test/java/de/ozgcloud/antragsraum/nachricht/RueckfrageTest.java b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/RueckfrageTest.java new file mode 100644 index 0000000000000000000000000000000000000000..402647a4bba932e22064aa6f11e52e966def688b --- /dev/null +++ b/server/src/test/java/de/ozgcloud/antragsraum/nachricht/RueckfrageTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2024. Das Land Schleswig-Holstein vertreten durch den Ministerpräsidenten + * des Landes Schleswig-Holstein Staatskanzlei Abteilung Digitalisierung und zentrales IT-Management der Landesregierung. + * + * Lizenziert unter der EUPL, Version 1.2 oder - sobald + * diese von der Europäischen Kommission genehmigt wurden - + * Folgeversionen der EUPL ("Lizenz"); + * Sie dürfen dieses Werk ausschließlich gemäß + * dieser Lizenz nutzen. + * Eine Kopie der Lizenz finden Sie hier: + * + * https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + * + * Sofern nicht durch anwendbare Rechtsvorschriften + * gefordert oder in schriftlicher Form vereinbart, wird + * die unter der Lizenz verbreitete Software "so wie sie + * ist", OHNE JEGLICHE GEWÄHRLEISTUNG ODER BEDINGUNGEN - + * ausdrücklich oder stillschweigend - verbreitet. + * Die sprachspezifischen Genehmigungen und Beschränkungen + * unter der Lizenz sind dem Lizenztext zu entnehmen. + */ + +package de.ozgcloud.antragsraum.nachricht; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +class RueckfrageTest { + final Rueckfrage rueckfrage = RueckfrageTestFactory.createRueckfrage(); + + @Test + void shouldNotPrintNachrichtText() { + String rueckfrageString = rueckfrage.toString(); + + assertThat(rueckfrageString).doesNotContain(NachrichtTestFactory.TEXT); + } + + @Test + void shouldNotPrintAnswers() { + String rueckfrageString = rueckfrage.toString(); + + assertThat(rueckfrageString).contains("antworten=<...masked for privacy..>"); + } +} \ No newline at end of file diff --git a/server/src/test/java/de/ozgcloud/antragsraum/security/AuthenticationControllerTest.java b/server/src/test/java/de/ozgcloud/antragsraum/security/AuthenticationControllerTest.java index 7f294ddd7cb39af587cbd4be2236884d58c5bbbc..9ab867230769f1fd56ddea5eee1a880c39c8f39a 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/security/AuthenticationControllerTest.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/security/AuthenticationControllerTest.java @@ -34,7 +34,10 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.HttpStatusCode; +import org.springframework.http.HttpStatus; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; @ExtendWith(MockitoExtension.class) class AuthenticationControllerTest { @@ -48,6 +51,9 @@ class AuthenticationControllerTest { @Mock private JwtTokenProvider tokenProvider; + @Mock + private SecurityContextLogoutHandler logoutHandler; + @Nested class TestLogin { private AuthCode code; @@ -78,7 +84,7 @@ class AuthenticationControllerTest { void shouldHaveStatusOk() { var tokenResponse = authenticationController.login(code); - assertThat(tokenResponse.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(200)); + assertThat(tokenResponse.getStatusCode()).isEqualTo(HttpStatus.OK); } @Test @@ -119,16 +125,16 @@ class AuthenticationControllerTest { void shouldCreateToken() { when(inMemoryUserDetailService.getUser(code)).thenReturn(Optional.of(UserTestFactory.create())); - var token = authenticationController.refresh(code); + var tokenResponse = authenticationController.refresh(code); - assertThat(token.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(200)); + assertThat(tokenResponse.getStatusCode()).isEqualTo(HttpStatus.OK); } @Test void shouldNotCreateToken() { - var token = authenticationController.refresh(code); + var tokenResponse = authenticationController.refresh(code); - assertThat(token.getStatusCode()).isEqualTo(HttpStatusCode.valueOf(403)); + assertThat(tokenResponse.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); } @Test @@ -140,4 +146,39 @@ class AuthenticationControllerTest { verify(inMemoryUserDetailService).updateRefreshCodeOf(any(User.class)); } } + + @Nested + class TestLogout { + private final MockHttpServletRequest request = new MockHttpServletRequest(); + private final MockHttpServletResponse response = new MockHttpServletResponse(); + private final User user = UserTestFactory.create(); + + @Test + void shouldHaveStatusOk() { + var res = authenticationController.logout(request, response, user); + + assertThat(res.getStatusCode()).isEqualTo(HttpStatus.OK); + } + + @Test + void shouldCallUserDetailService() { + authenticationController.logout(request, response, user); + + verify(inMemoryUserDetailService).logout(user); + } + + @Test + void shouldNotCallUserDetailService() { + authenticationController.logout(request, response, null); + + verify(inMemoryUserDetailService, never()).logout(user); + } + + @Test + void shouldCallLogoutHandler() { + authenticationController.logout(request, response, user); + + verify(logoutHandler).logout(eq(request), eq(response), any()); + } + } } \ No newline at end of file diff --git a/server/src/test/java/de/ozgcloud/antragsraum/security/AuthenticationHelperTest.java b/server/src/test/java/de/ozgcloud/antragsraum/security/AuthenticationHelperTest.java index 83872d53ece4b0871aaf944c8ce0ed6b9a601eb9..ddc9500135309d5539da86d9d516a7ab8a27cd36 100644 --- a/server/src/test/java/de/ozgcloud/antragsraum/security/AuthenticationHelperTest.java +++ b/server/src/test/java/de/ozgcloud/antragsraum/security/AuthenticationHelperTest.java @@ -75,7 +75,7 @@ class AuthenticationHelperTest { } @Test - void shouldGetEmptySamlToken() { + void shouldGetEmptySamlTokenBecauseWrongTokenType() { var authenticated = mock(RememberMeAuthenticationToken.class); var context = mock(SecurityContext.class); when(context.getAuthentication()).thenReturn(authenticated); @@ -85,5 +85,18 @@ class AuthenticationHelperTest { assertThat(samlToken).isEqualTo(AuthenticationHelper.NO_SAML_TOKEN); } + + @Test + void shouldGetEmptySamlTokenBecauseWrongUserType() { + var authenticated = mock(UsernamePasswordAuthenticationToken.class); + var context = mock(SecurityContext.class); + when(authenticated.getPrincipal()).thenReturn("test"); + when(context.getAuthentication()).thenReturn(authenticated); + SecurityContextHolder.setContext(context); + + var samlToken = AuthenticationHelper.getSamlToken(); + + assertThat(samlToken).isEqualTo(AuthenticationHelper.NO_SAML_TOKEN); + } } } \ No newline at end of file