diff --git a/siva-parent/siva-webapp/pom.xml b/siva-parent/siva-webapp/pom.xml
index a071f0bf8..d0101d13d 100644
--- a/siva-parent/siva-webapp/pom.xml
+++ b/siva-parent/siva-webapp/pom.xml
@@ -123,6 +123,14 @@
json
test
+
+
+ ee.openid.siva
+ validation-commons
+ ${project.version}
+ test
+ test-jar
+
diff --git a/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/ValidationExceptionHandler.java b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/ValidationExceptionHandler.java
index 5a50245eb..e2ac3c786 100644
--- a/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/ValidationExceptionHandler.java
+++ b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/ValidationExceptionHandler.java
@@ -21,6 +21,7 @@
import ee.openeid.siva.validation.exception.MalformedSignatureFileException;
import ee.openeid.siva.validation.exception.ValidationServiceException;
import ee.openeid.siva.validation.service.signature.policy.InvalidPolicyException;
+import ee.openeid.siva.webapp.request.limitation.RequestSizeLimitExceededException;
import ee.openeid.siva.webapp.response.erroneus.RequestValidationError;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource;
@@ -85,6 +86,14 @@ public RequestValidationError handleInvalidPolicyException(InvalidPolicyExceptio
return requestValidationError;
}
+ @ExceptionHandler(RequestSizeLimitExceededException.class)
+ @ResponseStatus(value = HttpStatus.BAD_REQUEST)
+ public RequestValidationError handleRequestSizeLimitExceededException(RequestSizeLimitExceededException e) {
+ RequestValidationError requestValidationError = new RequestValidationError();
+ requestValidationError.addFieldError("request", e.getMessage());
+ return requestValidationError;
+ }
+
private String getMessage(String key) {
return messageSource.getMessage(key, null, null);
diff --git a/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/configuration/HttpRequestLimitConfigurationProperties.java b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/configuration/HttpRequestLimitConfigurationProperties.java
new file mode 100644
index 000000000..eee15d9ac
--- /dev/null
+++ b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/configuration/HttpRequestLimitConfigurationProperties.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2024 Riigi Infosüsteemi Amet
+ *
+ * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
+ * the European Commission - subsequent versions of the EUPL (the "Licence");
+ * You may not use this work except in compliance with the Licence.
+ * You may obtain a copy of the Licence at:
+ *
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
+ * distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Licence for the specific language governing permissions and limitations under the Licence.
+ */
+
+package ee.openeid.siva.webapp.configuration;
+
+import ee.openeid.siva.webapp.request.validation.annotations.DataSizeMin;
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.util.unit.DataSize;
+import org.springframework.validation.annotation.Validated;
+
+@Data
+@Validated
+@Configuration
+@ConfigurationProperties(prefix = "siva.http.request")
+public class HttpRequestLimitConfigurationProperties {
+ @DataSizeMin(1L)
+ private DataSize maxRequestSizeLimit;
+}
diff --git a/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/limitation/ApplicationRequestSizeLimitFilter.java b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/limitation/ApplicationRequestSizeLimitFilter.java
new file mode 100644
index 000000000..4ac184943
--- /dev/null
+++ b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/limitation/ApplicationRequestSizeLimitFilter.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 Riigi Infosüsteemi Amet
+ *
+ * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
+ * the European Commission - subsequent versions of the EUPL (the "Licence");
+ * You may not use this work except in compliance with the Licence.
+ * You may obtain a copy of the Licence at:
+ *
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
+ * distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Licence for the specific language governing permissions and limitations under the Licence.
+ */
+
+package ee.openeid.siva.webapp.request.limitation;
+
+import ee.openeid.siva.webapp.configuration.HttpRequestLimitConfigurationProperties;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Order(Ordered.HIGHEST_PRECEDENCE)
+@Component
+@ConditionalOnProperty(prefix = "siva.http.request", name = "max-request-size-limit")
+public class ApplicationRequestSizeLimitFilter extends OncePerRequestFilter {
+
+ private final long maxRequestSizeLimit;
+
+ public ApplicationRequestSizeLimitFilter(HttpRequestLimitConfigurationProperties requestLimitProperties) {
+ maxRequestSizeLimit = requestLimitProperties.getMaxRequestSizeLimit().toBytes();
+ }
+
+ @Override
+ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
+ SizeLimitingHttpServletRequest sizeLimitingHttpServletRequest = new SizeLimitingHttpServletRequest(request, maxRequestSizeLimit);
+ filterChain.doFilter(sizeLimitingHttpServletRequest, response);
+ }
+}
diff --git a/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/limitation/RequestSizeLimitExceededException.java b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/limitation/RequestSizeLimitExceededException.java
new file mode 100644
index 000000000..ad6f7a4eb
--- /dev/null
+++ b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/limitation/RequestSizeLimitExceededException.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2024 Riigi Infosüsteemi Amet
+ *
+ * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
+ * the European Commission - subsequent versions of the EUPL (the "Licence");
+ * You may not use this work except in compliance with the Licence.
+ * You may obtain a copy of the Licence at:
+ *
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
+ * distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Licence for the specific language governing permissions and limitations under the Licence.
+ */
+
+package ee.openeid.siva.webapp.request.limitation;
+
+public class RequestSizeLimitExceededException extends RuntimeException {
+
+ public RequestSizeLimitExceededException(String message) {
+ super(message);
+ }
+}
diff --git a/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/limitation/SizeLimitingHttpServletRequest.java b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/limitation/SizeLimitingHttpServletRequest.java
new file mode 100644
index 000000000..4a98ebe63
--- /dev/null
+++ b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/limitation/SizeLimitingHttpServletRequest.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 Riigi Infosüsteemi Amet
+ *
+ * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
+ * the European Commission - subsequent versions of the EUPL (the "Licence");
+ * You may not use this work except in compliance with the Licence.
+ * You may obtain a copy of the Licence at:
+ *
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
+ * distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Licence for the specific language governing permissions and limitations under the Licence.
+ */
+
+package ee.openeid.siva.webapp.request.limitation;
+
+import lombok.Getter;
+
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import java.io.IOException;
+
+@Getter
+public class SizeLimitingHttpServletRequest extends HttpServletRequestWrapper {
+ private final long maximumAllowedReadLimit;
+
+ public SizeLimitingHttpServletRequest(HttpServletRequest request, long maximumAllowedReadLimit) {
+ super(request);
+ this.maximumAllowedReadLimit = maximumAllowedReadLimit;
+ }
+
+ @Override
+ public ServletInputStream getInputStream() throws IOException {
+ ensureContentLengthDoesNotExceedLimit();
+
+ return new SizeLimitingServletInputStream(super.getInputStream(), maximumAllowedReadLimit);
+ }
+
+ private void ensureContentLengthDoesNotExceedLimit() {
+ long contentLength = getContentLengthLong();
+
+ if (contentLength > maximumAllowedReadLimit) {
+ throw new RequestSizeLimitExceededException(String.format(
+ "Request content-length (%d bytes) exceeds request size limit (%d bytes)",
+ contentLength, maximumAllowedReadLimit
+ ));
+ }
+ }
+}
diff --git a/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/limitation/SizeLimitingServletInputStream.java b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/limitation/SizeLimitingServletInputStream.java
new file mode 100644
index 000000000..5e24f8b6d
--- /dev/null
+++ b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/limitation/SizeLimitingServletInputStream.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2024 Riigi Infosüsteemi Amet
+ *
+ * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
+ * the European Commission - subsequent versions of the EUPL (the "Licence");
+ * You may not use this work except in compliance with the Licence.
+ * You may obtain a copy of the Licence at:
+ *
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
+ * distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Licence for the specific language governing permissions and limitations under the Licence.
+ */
+
+package ee.openeid.siva.webapp.request.limitation;
+
+import lombok.NonNull;
+import lombok.RequiredArgsConstructor;
+
+import javax.servlet.ReadListener;
+import javax.servlet.ServletInputStream;
+import java.io.IOException;
+
+@RequiredArgsConstructor
+public class SizeLimitingServletInputStream extends ServletInputStream {
+
+ private final @NonNull ServletInputStream servletInputStream;
+ private final long maximumAllowedReadLimit;
+
+ private long bytesRead;
+
+ @Override
+ public int available() throws IOException {
+ return servletInputStream.available();
+ }
+
+ @Override
+ public void close() throws IOException {
+ servletInputStream.close();
+ }
+
+ @Override
+ public boolean isFinished() {
+ return servletInputStream.isFinished();
+ }
+
+ @Override
+ public boolean isReady() {
+ return servletInputStream.isReady();
+ }
+
+ @Override
+ public synchronized void mark(int readlimit) {
+ throw new UnsupportedOperationException("Mark not supported");
+ }
+
+ @Override
+ public void setReadListener(ReadListener listener) {
+ throw new UnsupportedOperationException("Setting read listener not supported");
+ }
+
+ @Override
+ public int read() throws IOException {
+ final int result = servletInputStream.read();
+
+ if (result >= 0) {
+ ++bytesRead;
+ ensureLimitNotExceeded();
+ }
+
+ return result;
+ }
+
+ @Override
+ public int read(byte[] b, int off, int len) throws IOException {
+ final int result = servletInputStream.read(b, off, calculateMaximumBytesToRead(len));
+
+ if (result >= 0) {
+ bytesRead += result;
+ ensureLimitNotExceeded();
+ }
+
+ return result;
+ }
+
+ @Override
+ public synchronized void reset() {
+ throw new UnsupportedOperationException("Mark not supported");
+ }
+
+ private int calculateMaximumBytesToRead(int requestedBytesToRead) {
+ final long remainingAllowedReadLength = maximumAllowedReadLimit - bytesRead;
+ return remainingAllowedReadLength < requestedBytesToRead
+ ? (int) (remainingAllowedReadLength + 1)
+ : requestedBytesToRead;
+ }
+
+ private void ensureLimitNotExceeded() {
+ if (bytesRead > maximumAllowedReadLimit) {
+ throw new RequestSizeLimitExceededException(String.format(
+ "Request body length exceeds request size limit (%d bytes)",
+ maximumAllowedReadLimit
+ ));
+ }
+ }
+}
diff --git a/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/validation/annotations/DataSizeMin.java b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/validation/annotations/DataSizeMin.java
new file mode 100644
index 000000000..ee086334c
--- /dev/null
+++ b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/validation/annotations/DataSizeMin.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 Riigi Infosüsteemi Amet
+ *
+ * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
+ * the European Commission - subsequent versions of the EUPL (the "Licence");
+ * You may not use this work except in compliance with the Licence.
+ * You may obtain a copy of the Licence at:
+ *
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
+ * distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Licence for the specific language governing permissions and limitations under the Licence.
+ */
+
+package ee.openeid.siva.webapp.request.validation.annotations;
+
+import ee.openeid.siva.webapp.request.validation.validators.DataSizeMinValidator;
+import org.springframework.util.unit.DataUnit;
+
+import javax.validation.Constraint;
+import javax.validation.Payload;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+@Constraint(validatedBy = DataSizeMinValidator.class)
+public @interface DataSizeMin {
+
+ String message() default "must not be less than {value} {unit}";
+ Class>[] groups() default {};
+ Class extends Payload>[] payload() default {};
+ long value() default 0L;
+ DataUnit unit() default DataUnit.BYTES;
+}
diff --git a/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/validation/validators/DataSizeMinValidator.java b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/validation/validators/DataSizeMinValidator.java
new file mode 100644
index 000000000..b9b922ab9
--- /dev/null
+++ b/siva-parent/siva-webapp/src/main/java/ee/openeid/siva/webapp/request/validation/validators/DataSizeMinValidator.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2024 Riigi Infosüsteemi Amet
+ *
+ * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
+ * the European Commission - subsequent versions of the EUPL (the "Licence");
+ * You may not use this work except in compliance with the Licence.
+ * You may obtain a copy of the Licence at:
+ *
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
+ * distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Licence for the specific language governing permissions and limitations under the Licence.
+ */
+
+package ee.openeid.siva.webapp.request.validation.validators;
+
+import ee.openeid.siva.webapp.request.validation.annotations.DataSizeMin;
+import org.springframework.util.unit.DataSize;
+
+import javax.validation.ConstraintValidator;
+import javax.validation.ConstraintValidatorContext;
+
+public class DataSizeMinValidator implements ConstraintValidator {
+
+ private DataSize minAllowedDataSize;
+
+ @Override
+ public void initialize(DataSizeMin constraintAnnotation) {
+ minAllowedDataSize = DataSize.of(constraintAnnotation.value(), constraintAnnotation.unit());
+ }
+
+ @Override
+ public boolean isValid(DataSize dataSize, ConstraintValidatorContext constraintValidatorContext) {
+ return (dataSize == null) || dataSize.compareTo(minAllowedDataSize) >= 0;
+ }
+
+}
diff --git a/siva-parent/siva-webapp/src/main/resources/application.yml b/siva-parent/siva-webapp/src/main/resources/application.yml
index eab6a8ee7..3c58de150 100644
--- a/siva-parent/siva-webapp/src/main/resources/application.yml
+++ b/siva-parent/siva-webapp/src/main/resources/application.yml
@@ -1,8 +1,12 @@
-server.max-http-post-size: 13981016 # 10 MB + base64 overhead (ceil(10MB / 3) * 4)
spring.http.encoding.charset: UTF-8 # Charset of HTTP requests and responses. Added to the "Content-Type" header if not set explicitly.
spring.http.encoding.enabled: true # Enable http encoding support.
spring.http.encoding.force: true # Force the encoding to the configured charset on HTTP requests and responses.
+siva:
+ http:
+ request:
+ max-request-size-limit: 26MB # 20 MB + base64 overhead (ceil(10MB / 3) * 4)
+
management:
health:
diskspace:
diff --git a/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/configuration/HttpRequestLimitConfigurationPropertiesTest.java b/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/configuration/HttpRequestLimitConfigurationPropertiesTest.java
new file mode 100644
index 000000000..68afff115
--- /dev/null
+++ b/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/configuration/HttpRequestLimitConfigurationPropertiesTest.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2024 Riigi Infosüsteemi Amet
+ *
+ * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
+ * the European Commission - subsequent versions of the EUPL (the "Licence");
+ * You may not use this work except in compliance with the Licence.
+ * You may obtain a copy of the Licence at:
+ *
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
+ * distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Licence for the specific language governing permissions and limitations under the Licence.
+ */
+
+package ee.openeid.siva.webapp.configuration;
+
+import ee.openeid.siva.validation.helper.AbstractValidationTest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.springframework.util.unit.DataSize;
+
+class HttpRequestLimitConfigurationPropertiesTest extends AbstractValidationTest {
+
+ HttpRequestLimitConfigurationProperties configurationProperties;
+
+ @BeforeEach
+ void setUpConfigurationProperties() {
+ configurationProperties = new HttpRequestLimitConfigurationProperties();
+ }
+
+ @Test
+ void validate_WhenMaxRequestSizeLimitIsMissing_Succeeds() {
+ configurationProperties.setMaxRequestSizeLimit(null);
+ validateAndExpectNoErrors(configurationProperties);
+ }
+
+ @ParameterizedTest
+ @ValueSource(longs = {1L, 1024L, Long.MAX_VALUE})
+ void validate_WhenMaxRequestSizeLimitIsMoreOrEqualThanOneByte_Succeeds(long sizeInBytes) {
+ configurationProperties.setMaxRequestSizeLimit(DataSize.ofBytes(sizeInBytes));
+ validateAndExpectNoErrors(configurationProperties);
+ }
+
+ @ParameterizedTest
+ @ValueSource(longs = {Long.MIN_VALUE, -1L, 0L})
+ void validate_WhenMaxRequestSizeLimitIsLessThanOneByte_Fails(long sizeInBytes) {
+ configurationProperties.setMaxRequestSizeLimit(DataSize.ofBytes(sizeInBytes));
+ validateAndExpectOneError(configurationProperties, "maxRequestSizeLimit", "must not be less than 1 BYTES");
+ }
+
+}
diff --git a/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/request/limitation/ApplicationRequestSizeLimitFilterTest.java b/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/request/limitation/ApplicationRequestSizeLimitFilterTest.java
new file mode 100644
index 000000000..864c95642
--- /dev/null
+++ b/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/request/limitation/ApplicationRequestSizeLimitFilterTest.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2024 Riigi Infosüsteemi Amet
+ *
+ * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
+ * the European Commission - subsequent versions of the EUPL (the "Licence");
+ * You may not use this work except in compliance with the Licence.
+ * You may obtain a copy of the Licence at:
+ *
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
+ * distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Licence for the specific language governing permissions and limitations under the Licence.
+ */
+
+package ee.openeid.siva.webapp.request.limitation;
+
+import ee.openeid.siva.webapp.configuration.HttpRequestLimitConfigurationProperties;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.util.unit.DataSize;
+
+import javax.servlet.FilterChain;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+@ExtendWith(MockitoExtension.class)
+public class ApplicationRequestSizeLimitFilterTest {
+
+ @Mock
+ private HttpServletRequest request;
+ @Mock
+ private HttpServletResponse response;
+ @Mock
+ private FilterChain filterChain;
+
+ @ParameterizedTest
+ @MethodSource("dataSizeProvider")
+ void doFilterInternal_WhenApplicationRequestSizeLimitFilterIsSet_WrapperIsInjectedToFilterChain(DataSize maxRequestSizeLimit) throws Exception {
+ HttpRequestLimitConfigurationProperties requestLimitProperties = new HttpRequestLimitConfigurationProperties();
+ requestLimitProperties.setMaxRequestSizeLimit(maxRequestSizeLimit);
+ ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(SizeLimitingHttpServletRequest.class);
+
+ ApplicationRequestSizeLimitFilter limitFilter = new ApplicationRequestSizeLimitFilter(requestLimitProperties);
+
+ limitFilter.doFilterInternal(request, response, filterChain);
+
+ verify(filterChain).doFilter(argumentCaptor.capture(), eq(response));
+
+ SizeLimitingHttpServletRequest capturedRequest = argumentCaptor.getValue();
+
+ assertEquals(maxRequestSizeLimit.toBytes(), capturedRequest.getMaximumAllowedReadLimit());
+ verifyNoMoreInteractions(filterChain);
+ verifyNoInteractions(request, response);
+ }
+
+ private static Stream dataSizeProvider() {
+ return Stream.of(
+ DataSize.ofBytes(1024),
+ DataSize.ofKilobytes(10240),
+ DataSize.ofMegabytes(1024)
+ );
+ }
+}
diff --git a/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/request/limitation/SizeLimitingHttpServletRequestTest.java b/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/request/limitation/SizeLimitingHttpServletRequestTest.java
new file mode 100644
index 000000000..0ff8875af
--- /dev/null
+++ b/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/request/limitation/SizeLimitingHttpServletRequestTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2024 Riigi Infosüsteemi Amet
+ *
+ * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
+ * the European Commission - subsequent versions of the EUPL (the "Licence");
+ * You may not use this work except in compliance with the Licence.
+ * You may obtain a copy of the Licence at:
+ *
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
+ * distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Licence for the specific language governing permissions and limitations under the Licence.
+ */
+
+package ee.openeid.siva.webapp.request.limitation;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.HttpServletRequest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+@ExtendWith(MockitoExtension.class)
+public class SizeLimitingHttpServletRequestTest {
+
+ @Mock
+ private HttpServletRequest httpServletRequest;
+
+ @ParameterizedTest
+ @ValueSource(longs = {Long.MIN_VALUE, -1L, 0L, 1L})
+ void getInputStream_WhenLimitIsBiggerThanContentLength_ReturnsServletInputStream(long contentLengthLong) throws Exception {
+ ServletInputStream servletInputStream = mock(ServletInputStream.class);
+ doReturn(contentLengthLong).when(httpServletRequest).getContentLengthLong();
+ doReturn(servletInputStream).when(httpServletRequest).getInputStream();
+
+ SizeLimitingHttpServletRequest limitingHttpServletRequest = new SizeLimitingHttpServletRequest(
+ httpServletRequest, 1L);
+
+ ServletInputStream result = limitingHttpServletRequest.getInputStream();
+
+ assertEquals(SizeLimitingServletInputStream.class, result.getClass());
+ verifyNoMoreInteractions(httpServletRequest);
+ verifyNoInteractions(servletInputStream);
+ }
+
+ @ParameterizedTest
+ @ValueSource(longs = {2L, 3L, Long.MAX_VALUE})
+ void getInputStream_WhenLimitIsSmallerThanContentLength_ThrowsException(long contentLengthLong) {
+ ServletInputStream servletInputStream = mock(ServletInputStream.class);
+ doReturn(contentLengthLong).when(httpServletRequest).getContentLengthLong();
+
+ SizeLimitingHttpServletRequest limitingHttpServletRequest = new SizeLimitingHttpServletRequest(
+ httpServletRequest, 1L);
+
+ RequestSizeLimitExceededException caughtException = assertThrows(
+ RequestSizeLimitExceededException.class,
+ limitingHttpServletRequest::getInputStream
+ );
+
+ assertEquals("Request content-length (" + contentLengthLong + " bytes) exceeds request size limit (1 bytes)",
+ caughtException.getMessage());
+ verifyNoMoreInteractions(httpServletRequest);
+ verifyNoInteractions(servletInputStream);
+ }
+}
diff --git a/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/request/limitation/SizeLimitingServletInputStreamTest.java b/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/request/limitation/SizeLimitingServletInputStreamTest.java
new file mode 100644
index 000000000..9d60f9d2e
--- /dev/null
+++ b/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/request/limitation/SizeLimitingServletInputStreamTest.java
@@ -0,0 +1,510 @@
+/*
+ * Copyright 2024 Riigi Infosüsteemi Amet
+ *
+ * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
+ * the European Commission - subsequent versions of the EUPL (the "Licence");
+ * You may not use this work except in compliance with the Licence.
+ * You may obtain a copy of the Licence at:
+ *
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
+ * distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Licence for the specific language governing permissions and limitations under the Licence.
+ */
+
+package ee.openeid.siva.webapp.request.limitation;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.stubbing.Stubber;
+
+import javax.servlet.ReadListener;
+import javax.servlet.ServletInputStream;
+
+import java.io.IOException;
+import java.util.function.IntFunction;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.sameInstance;
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+@ExtendWith(MockitoExtension.class)
+public class SizeLimitingServletInputStreamTest {
+
+ @Mock
+ private ServletInputStream servletInputStream;
+
+ @Test
+ void available_WhenWrappedInputStreamAvailableReturnsGivenValue_ReturnsTheValue() throws IOException {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(0L);
+ doReturn(15).when(servletInputStream).available();
+
+ int result = limitingServletInputStream.available();
+
+ assertThat(result, equalTo(15));
+ verify(servletInputStream).available();
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @Test
+ void available_WhenWrappedInputStreamAvailableThrowsIOException_ReThrowsTheException() throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(0L);
+ IOException ioException = new IOException("Exception message");
+ doThrow(ioException).when(servletInputStream).available();
+
+ IOException caughtException = assertThrows(
+ IOException.class,
+ limitingServletInputStream::available
+ );
+
+ assertThat(caughtException, sameInstance(ioException));
+ verify(servletInputStream).available();
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @Test
+ void close_WhenWrappedInputStreamCloseSucceeds_Succeeds() throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(0L);
+
+ limitingServletInputStream.close();
+
+ verify(servletInputStream).close();
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @Test
+ void close_WhenWrappedInputStreamCloseThrowsIOException_ReThrowsTheException() throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(0L);
+ IOException ioException = new IOException("Exception message");
+ doThrow(ioException).when(servletInputStream).close();
+
+ IOException caughtException = assertThrows(
+ IOException.class,
+ limitingServletInputStream::close
+ );
+
+ assertThat(caughtException, sameInstance(ioException));
+ verify(servletInputStream).close();
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void idFinished_WhenWrappedInputStreamIsFinishedReturnsGivenValue_ReturnsTheValue(boolean value) {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(0L);
+ doReturn(value).when(servletInputStream).isFinished();
+
+ boolean result = limitingServletInputStream.isFinished();
+
+ assertThat(result, equalTo(value));
+ verify(servletInputStream).isFinished();
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void idReady_WhenWrappedInputStreamIsReadyReturnsGivenValue_ReturnsTheValue(boolean value) {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(0L);
+ doReturn(value).when(servletInputStream).isReady();
+
+ boolean result = limitingServletInputStream.isReady();
+
+ assertThat(result, equalTo(value));
+ verify(servletInputStream).isReady();
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @Test
+ void mark_ThrowsUnsupportedOperationExceptionWithoutInteractingWithWrappedInputStream() {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(0L);
+
+ UnsupportedOperationException caughtException = assertThrows(
+ UnsupportedOperationException.class,
+ () -> limitingServletInputStream.mark(1)
+ );
+
+ assertThat(caughtException.getMessage(), equalTo("Mark not supported"));
+ verifyNoInteractions(servletInputStream);
+ }
+
+ @Test
+ void markSupported_ReturnsFalseWithoutInteractingWithWrappedInputStream() {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(0L);
+
+ boolean result = limitingServletInputStream.markSupported();
+
+ assertThat(result, equalTo(false));
+ verifyNoInteractions(servletInputStream);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {1, 2, 3})
+ void read_WhenReadIsCalledLessTimesThanOrAsManyTimesAsReadLimit_ReturnsReadBytes(int timesToRead) throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(3L);
+ doReturnMultiple(timesToRead, i -> i).when(servletInputStream).read();
+
+ for (int i = 0; i < timesToRead; ++i) {
+ int result = limitingServletInputStream.read();
+
+ assertThat(result, equalTo(i));
+ }
+ verify(servletInputStream, times(timesToRead)).read();
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 2})
+ void read_WhenReadIsCalledMoreTimesThanReadLimit_ThrowsLimitExceededException(int limit) throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(limit);
+ doReturnMultiple(limit + 1, i -> i).when(servletInputStream).read();
+
+ for (int i = 0; i < limit; ++i) {
+ int result = limitingServletInputStream.read();
+ assertThat(result, equalTo(i));
+ }
+
+ RequestSizeLimitExceededException caughtException = assertThrows(
+ RequestSizeLimitExceededException.class,
+ limitingServletInputStream::read
+ );
+
+ assertThat(caughtException.getMessage(), equalTo(String.format(
+ "Request body length exceeds request size limit (%d bytes)",
+ limit
+ )));
+ verify(servletInputStream, times(limit + 1)).read();
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @Test
+ void read_WhenReadIsCalledMoreTimesThanReadLimitButStreamEndsBefore_ReturnsReadBytes() throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(1L);
+ doReturn(7, -1).when(servletInputStream).read();
+
+ int result1 = limitingServletInputStream.read();
+ int result2 = limitingServletInputStream.read();
+
+ assertThat(result1, equalTo(7));
+ assertThat(result2, equalTo(-1));
+ verify(servletInputStream, times(2)).read();
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {1, 2, 3})
+ void read_WhenReadIsCalledWithArrayShorterThanOrAsLongAsReadLimit_ReturnsNumberOfBytesRead(
+ int lengthToRead
+ ) throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(3L);
+ byte[] buffer = new byte[lengthToRead];
+ doAnswerToBulkRead(lengthToRead, i -> (byte) i).when(servletInputStream).read(buffer, 0, lengthToRead);
+
+ int result = limitingServletInputStream.read(buffer);
+
+ assertThat(result, equalTo(lengthToRead));
+ assertArrayEquals(generateByteArray(lengthToRead, i -> (byte) i), buffer);
+ verify(servletInputStream).read(buffer, 0, lengthToRead);
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 2})
+ void read_WhenReadIsCalledWithArrayLongerThanReadLimit_ThrowsLimitExceededException(int limit) throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(limit);
+ byte[] buffer = new byte[limit + 1];
+ doAnswerToBulkRead(buffer.length, i -> (byte) i).when(servletInputStream).read(buffer, 0, limit + 1);
+
+ RequestSizeLimitExceededException caughtException = assertThrows(
+ RequestSizeLimitExceededException.class,
+ () -> limitingServletInputStream.read(buffer)
+ );
+
+ assertThat(caughtException.getMessage(), equalTo(String.format(
+ "Request body length exceeds request size limit (%d bytes)",
+ limit
+ )));
+ verify(servletInputStream).read(buffer, 0, limit + 1);
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @Test
+ void read_WhenReadIsCalledWithArrayLongerThanReadLimitButStreamEndsBefore_ReturnsNumberOfBytesRead() throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(1L);
+ byte[] buffer = new byte[2];
+ doAnswer(invocationOnMock -> {
+ invocationOnMock.getArgument(0, byte[].class)[0] = 7;
+ return 1;
+ }).when(servletInputStream).read(buffer, 0, 2);
+
+ int result = limitingServletInputStream.read(buffer);
+
+ assertThat(result, equalTo(1));
+ assertArrayEquals(new byte[] {7, 0}, buffer);
+ verify(servletInputStream).read(buffer, 0, 2);
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {1, 2, 3})
+ void read_WhenReadIsCalledWithLengthShorterThanOrAsLongAsReadLimit_ReturnsNumberOfBytesRead(
+ int lengthToRead
+ ) throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(3);
+ byte[] buffer = new byte[lengthToRead + 2];
+ doAnswerToBulkRead(lengthToRead, i -> (byte) i).when(servletInputStream).read(buffer, 1, lengthToRead);
+
+ int result = limitingServletInputStream.read(buffer, 1, lengthToRead);
+
+ assertThat(result, equalTo(lengthToRead));
+ assertArrayEquals(generateByteArray(lengthToRead + 2, i -> (i < 1 || i > lengthToRead) ? 0 : (byte) (i - 1)), buffer);
+ verify(servletInputStream).read(buffer, 1, lengthToRead);
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 2})
+ void read_WhenReadIsCalledWithLengthLongerThanReadLimit_ThrowsLimitExceededException(int limit) throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(limit);
+ byte[] buffer = new byte[limit + 2];
+ doAnswerToBulkRead(buffer.length, i -> (byte) i).when(servletInputStream).read(buffer, 1, limit + 1);
+
+ RequestSizeLimitExceededException caughtException = assertThrows(
+ RequestSizeLimitExceededException.class,
+ () -> limitingServletInputStream.read(buffer, 1, limit + 1)
+ );
+
+ assertThat(caughtException.getMessage(), equalTo(String.format(
+ "Request body length exceeds request size limit (%d bytes)",
+ limit
+ )));
+ verify(servletInputStream).read(buffer, 1, limit + 1);
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @Test
+ void read_WhenReadIsCalledWithLengthLongerThanReadLimitButStreamEndsBefore_ReturnsNumberOfBytesRead() throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(1L);
+ byte[] buffer = new byte[3];
+ doAnswer(invocationOnMock -> {
+ invocationOnMock.getArgument(0, byte[].class)[1] = 7;
+ return 1;
+ }).when(servletInputStream).read(buffer, 1, 2);
+
+ int result = limitingServletInputStream.read(buffer, 1, 2);
+
+ assertThat(result, equalTo(1));
+ assertArrayEquals(new byte[] {0, 7, 0}, buffer);
+ verify(servletInputStream).read(buffer, 1, 2);
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @Test
+ void readAllBytes_WhenStreamLengthIs0_DelegatesToBulkReadAndReturnsEmptyByteArray() throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(0L);
+ doReturn(-1).when(servletInputStream)
+ .read(any(byte[].class), eq(0), eq(1));
+
+ byte[] result = limitingServletInputStream.readAllBytes();
+
+ assertArrayEquals(new byte[0], result);
+ verify(servletInputStream).read(any(byte[].class), eq(0), eq(1));
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {1, 2})
+ void readAllBytes_WhenStreamLengthIsLessThanOrEqualToReadLimit_DelegatesToBulkReadAndReturnsEmptyByteArray(
+ int streamLength
+ ) throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(2L);
+ doAnswerToBulkRead(streamLength, i -> (byte) i).when(servletInputStream)
+ .read(any(byte[].class), eq(0), eq(3));
+ doReturn(-1).when(servletInputStream)
+ .read(any(byte[].class), eq(streamLength), eq(3 - streamLength));
+
+ byte[] result = limitingServletInputStream.readAllBytes();
+
+ assertArrayEquals(generateByteArray(streamLength, i -> (byte) i), result);
+ verify(servletInputStream).read(any(byte[].class), eq(0), eq(3));
+ verify(servletInputStream).read(any(byte[].class), eq(streamLength), eq(3 - streamLength));
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @Test
+ void readAllBytes_WhenStreamLengthIsMoreThanReadLimit_ThrowsLimitExceededException() throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(1L);
+ doAnswerToBulkRead(2, i -> (byte) i).when(servletInputStream)
+ .read(any(byte[].class), eq(0), eq(2));
+
+ RequestSizeLimitExceededException caughtException = assertThrows(
+ RequestSizeLimitExceededException.class,
+ limitingServletInputStream::readAllBytes
+ );
+
+ assertThat(
+ caughtException.getMessage(),
+ equalTo("Request body length exceeds request size limit (1 bytes)")
+ );
+ verify(servletInputStream).read(any(byte[].class), eq(0), eq(2));
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {0, 1, 2})
+ void readLine_WhenNewlineCharacterIsReachedBeforeReadLimit_DelegatesToReadAndReturnsNumberOfBytesRead(
+ int bytesBeforeNewline
+ ) throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(3L);
+ doReturnMultiple(bytesBeforeNewline + 1, i -> (i < bytesBeforeNewline) ? i : '\n')
+ .when(servletInputStream).read();
+ byte[] buffer = new byte[bytesBeforeNewline + 1];
+
+ int result = limitingServletInputStream.readLine(buffer, 0, buffer.length);
+
+ assertThat(result, equalTo(bytesBeforeNewline + 1));
+ assertArrayEquals(
+ generateByteArray(bytesBeforeNewline + 1, i -> (i < bytesBeforeNewline) ? (byte) i : (byte) '\n'),
+ buffer
+ );
+ verify(servletInputStream, times(bytesBeforeNewline + 1)).read();
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @Test
+ void readLine_WhenReadLimitIsReachedBeforeNewlineCharacter_ThrowsLimitExceededException() throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(0L);
+ doReturn((int) '\n').when(servletInputStream).read();
+ byte[] buffer = new byte[1];
+
+ RequestSizeLimitExceededException caughtException = assertThrows(
+ RequestSizeLimitExceededException.class,
+ () -> limitingServletInputStream.readLine(buffer, 0, buffer.length)
+ );
+
+ assertThat(
+ caughtException.getMessage(),
+ equalTo("Request body length exceeds request size limit (0 bytes)")
+ );
+ verify(servletInputStream).read();
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @Test
+ void reset_ThrowsUnsupportedOperationExceptionWithoutInteractingWithWrappedInputStream() {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(0L);
+
+ UnsupportedOperationException caughtException = assertThrows(
+ UnsupportedOperationException.class,
+ limitingServletInputStream::reset
+ );
+
+ assertThat(caughtException.getMessage(), equalTo("Mark not supported"));
+ verifyNoInteractions(servletInputStream);
+ }
+
+ @Test
+ void setReadListener_ThrowsUnsupportedOperationExceptionWithoutInteractingWithWrappedInputStream() {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(0L);
+ ReadListener readListener = mock(ReadListener.class);
+
+ UnsupportedOperationException caughtException = assertThrows(
+ UnsupportedOperationException.class,
+ () -> limitingServletInputStream.setReadListener(readListener)
+ );
+
+ assertThat(caughtException.getMessage(), equalTo("Setting read listener not supported"));
+ verifyNoInteractions(servletInputStream);
+ }
+
+ @ParameterizedTest
+ @ValueSource(ints = {1, 2})
+ void skip_WhenSkipIsCalledWithNumberLessThanOrEqualToReadLimit_ReturnsNumberOfBytesSkipped(
+ int bytesToSkip
+ ) throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(2L);
+ doReturn(bytesToSkip).when(servletInputStream).read(any(byte[].class), eq(0), anyInt());
+
+ long result = limitingServletInputStream.skip(bytesToSkip);
+
+ assertThat(result, equalTo((long) bytesToSkip));
+ verify(servletInputStream).read(any(byte[].class), eq(0), anyInt());
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ @Test
+ void skip_WhenSkipIsCalledWithNumberGreaterThanReadLimit_ReturnsNumberOfBytesSkipped() throws Exception {
+ SizeLimitingServletInputStream limitingServletInputStream = createLimitingInputStream(1L);
+ doReturn(2).when(servletInputStream).read(any(byte[].class), eq(0), anyInt());
+
+ RequestSizeLimitExceededException caughtException = assertThrows(
+ RequestSizeLimitExceededException.class,
+ () -> limitingServletInputStream.skip(2L)
+ );
+
+ assertThat(
+ caughtException.getMessage(),
+ equalTo("Request body length exceeds request size limit (1 bytes)")
+ );
+ verify(servletInputStream).read(any(byte[].class), eq(0), anyInt());
+ verifyNoMoreInteractions(servletInputStream);
+ }
+
+ private SizeLimitingServletInputStream createLimitingInputStream(long limit) {
+ return new SizeLimitingServletInputStream(servletInputStream, limit);
+ }
+
+ private static Stubber doReturnMultiple(int count, IntFunction elementResolver) {
+ if (count > 1) {
+ Object toBeReturned = elementResolver.apply(0);
+ Object[] toBeReturnedNext = new Object[count - 1];
+ for (int i = 0; i < toBeReturnedNext.length; ++i) {
+ toBeReturnedNext[i] = elementResolver.apply(i + 1);
+ }
+ return doReturn(toBeReturned, toBeReturnedNext);
+ } else if (count == 1) {
+ return doReturn(elementResolver.apply(0));
+ } else {
+ throw new IllegalArgumentException("Invalid count: " + count);
+ }
+ }
+
+ private static Stubber doAnswerToBulkRead(int maxLengthToRead, IntFunction byteResolver) {
+ return doAnswer(invocationOnMock -> {
+ byte[] array = invocationOnMock.getArgument(0, byte[].class);
+ int offset = invocationOnMock.getArgument(1, Integer.class);
+ int length = invocationOnMock.getArgument(2, Integer.class);
+ int bytesRead = Math.min(maxLengthToRead, length);
+ for (int i = 0; i < bytesRead; ++i) {
+ array[offset + i] = byteResolver.apply(i);
+ }
+ return bytesRead;
+ });
+ }
+
+ private static byte[] generateByteArray(int length, IntFunction byteResolver) {
+ byte[] array = new byte[length];
+ for (int i = 0; i < length; ++i) {
+ array[i] = byteResolver.apply(i);
+ }
+ return array;
+ }
+}
diff --git a/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/request/validation/DataSizeMinValidatorTest.java b/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/request/validation/DataSizeMinValidatorTest.java
new file mode 100644
index 000000000..51a0ab9bb
--- /dev/null
+++ b/siva-parent/siva-webapp/src/test/java/ee/openeid/siva/webapp/request/validation/DataSizeMinValidatorTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright 2024 Riigi Infosüsteemi Amet
+ *
+ * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
+ * the European Commission - subsequent versions of the EUPL (the "Licence");
+ * You may not use this work except in compliance with the Licence.
+ * You may obtain a copy of the Licence at:
+ *
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
+ * distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Licence for the specific language governing permissions and limitations under the Licence.
+ */
+
+package ee.openeid.siva.webapp.request.validation;
+
+import ee.openeid.siva.webapp.request.validation.annotations.DataSizeMin;
+import ee.openeid.siva.webapp.request.validation.validators.DataSizeMinValidator;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.util.unit.DataSize;
+import org.springframework.util.unit.DataUnit;
+
+import javax.validation.ConstraintValidatorContext;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+@ExtendWith(MockitoExtension.class)
+public class DataSizeMinValidatorTest {
+
+ @InjectMocks
+ private DataSizeMinValidator dataSizeMinValidator;
+
+ @Mock
+ private DataSizeMin dataSizeMinAnnotation;
+ @Mock
+ private ConstraintValidatorContext constraintValidatorContext;
+
+ @ParameterizedTest
+ @EnumSource(DataUnit.class)
+ void isValid_WhenDataSizeIsNull_ReturnsTrue(DataUnit unit) {
+ initializeAnnotatedLimitAndValidator(0L, unit);
+
+ boolean result = dataSizeMinValidator.isValid(null, constraintValidatorContext);
+
+ assertThat(result, equalTo(true));
+ verifyAllInteractions();
+ }
+
+ @ParameterizedTest
+ @EnumSource(DataUnit.class)
+ void isValid_WhenDataSizeIsEqualToAnnotatedLimit_ReturnsTrue(DataUnit unit) {
+ initializeAnnotatedLimitAndValidator(7L, unit);
+ DataSize dataSize = DataSize.of(7L, unit);
+
+ boolean result = dataSizeMinValidator.isValid(dataSize, constraintValidatorContext);
+
+ assertThat(result, equalTo(true));
+ verifyAllInteractions();
+ }
+
+ @ParameterizedTest
+ @EnumSource(DataUnit.class)
+ void isValid_WhenDataSizeIsGreaterThanAnnotatedLimit_ReturnsTrue(DataUnit unit) {
+ initializeAnnotatedLimitAndValidator(7L, unit);
+ DataSize dataSize = DataSize.ofBytes(
+ DataSize.of(7L, unit).toBytes() + 1L
+ );
+
+ boolean result = dataSizeMinValidator.isValid(dataSize, constraintValidatorContext);
+
+ assertThat(result, equalTo(true));
+ verifyAllInteractions();
+ }
+
+ @ParameterizedTest
+ @EnumSource(DataUnit.class)
+ void isValid_WhenDataSizeIsLessThanAnnotatedLimit_ReturnsFalse(DataUnit unit) {
+ initializeAnnotatedLimitAndValidator(7L, unit);
+ DataSize dataSize = DataSize.ofBytes(
+ DataSize.of(7L, unit).toBytes() - 1L
+ );
+
+ boolean result = dataSizeMinValidator.isValid(dataSize, constraintValidatorContext);
+
+ assertThat(result, equalTo(false));
+ verifyAllInteractions();
+ }
+
+ private void initializeAnnotatedLimitAndValidator(long value, DataUnit unit) {
+ doReturn(value).when(dataSizeMinAnnotation).value();
+ doReturn(unit).when(dataSizeMinAnnotation).unit();
+
+ dataSizeMinValidator.initialize(dataSizeMinAnnotation);
+ }
+
+ private void verifyAllInteractions() {
+ verify(dataSizeMinAnnotation).value();
+ verify(dataSizeMinAnnotation).unit();
+
+ verifyNoMoreInteractions(dataSizeMinAnnotation);
+ verifyNoInteractions(constraintValidatorContext);
+ }
+}
diff --git a/validation-services-parent/validation-commons/pom.xml b/validation-services-parent/validation-commons/pom.xml
index 56c46d4b2..c143fe4b1 100644
--- a/validation-services-parent/validation-commons/pom.xml
+++ b/validation-services-parent/validation-commons/pom.xml
@@ -95,6 +95,11 @@
+
+ org.springframework.boot
+ spring-boot-starter-validation
+ test
+
diff --git a/validation-services-parent/validation-commons/src/test/java/ee/openeid/siva/validation/helper/AbstractValidationTest.java b/validation-services-parent/validation-commons/src/test/java/ee/openeid/siva/validation/helper/AbstractValidationTest.java
new file mode 100644
index 000000000..f4519c969
--- /dev/null
+++ b/validation-services-parent/validation-commons/src/test/java/ee/openeid/siva/validation/helper/AbstractValidationTest.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2024 Riigi Infosüsteemi Amet
+ *
+ * Licensed under the EUPL, Version 1.1 or – as soon they will be approved by
+ * the European Commission - subsequent versions of the EUPL (the "Licence");
+ * You may not use this work except in compliance with the Licence.
+ * You may obtain a copy of the Licence at:
+ *
+ * https://joinup.ec.europa.eu/software/page/eupl
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
+ * distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the Licence for the specific language governing permissions and limitations under the Licence.
+ */
+
+package ee.openeid.siva.validation.helper;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.Validation;
+import javax.validation.Validator;
+import javax.validation.ValidatorFactory;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+public class AbstractValidationTest {
+
+ private static ValidatorFactory validatorFactory;
+
+ protected Validator validator;
+
+ @BeforeAll
+ public static void setUpDefaultValidatorFactory() {
+ validatorFactory = Validation.buildDefaultValidatorFactory();
+ }
+
+ @BeforeEach
+ public void setUpDefaultValidator() {
+ validator = validatorFactory.getValidator();
+ }
+
+ protected void validateAndExpectNoErrors(Object objectToValidate) {
+ Set> violations = validator.validate(objectToValidate);
+ assertThat(violations, equalTo(Set.of()));
+ }
+
+ protected void validateAndExpectOneError(Object objectToValidate, String path, String message) {
+ Set> violations = validator.validate(objectToValidate);
+ assertEquals(1, violations.size(), () -> "Expected exactly one violation, found:" +
+ violations.stream().map(ConstraintViolation::getMessage).collect(Collectors.joining(System.lineSeparator()))
+ );
+
+ ConstraintViolation