From e68feb1b54c650c24d71b66866796f9e4acc6cee Mon Sep 17 00:00:00 2001 From: Andreas Hufler Date: Tue, 2 Dec 2025 14:19:06 +0100 Subject: [PATCH] add http testing tools --- pom.xml | 15 +- .../testing/web/request/Delete.java | 30 ++ .../springboot/testing/web/request/Get.java | 30 ++ .../testing/web/request/HttpTestClient.java | 45 +++ .../testing/web/request/HttpTestSecurity.java | 164 +++++++++++ .../testing/web/request/MultipartRequest.java | 51 ++++ .../springboot/testing/web/request/Patch.java | 30 ++ .../testing/web/request/PatchMultipart.java | 29 ++ .../springboot/testing/web/request/Post.java | 30 ++ .../testing/web/request/PostMultipart.java | 29 ++ .../springboot/testing/web/request/Put.java | 30 ++ .../testing/web/request/PutMultipart.java | 29 ++ .../testing/web/request/Request.java | 274 ++++++++++++++++++ .../web/request/RequestSecurityAssert.java | 32 ++ 14 files changed, 816 insertions(+), 2 deletions(-) create mode 100644 src/main/java/it/aboutbits/springboot/testing/web/request/Delete.java create mode 100644 src/main/java/it/aboutbits/springboot/testing/web/request/Get.java create mode 100644 src/main/java/it/aboutbits/springboot/testing/web/request/HttpTestClient.java create mode 100644 src/main/java/it/aboutbits/springboot/testing/web/request/HttpTestSecurity.java create mode 100644 src/main/java/it/aboutbits/springboot/testing/web/request/MultipartRequest.java create mode 100644 src/main/java/it/aboutbits/springboot/testing/web/request/Patch.java create mode 100644 src/main/java/it/aboutbits/springboot/testing/web/request/PatchMultipart.java create mode 100644 src/main/java/it/aboutbits/springboot/testing/web/request/Post.java create mode 100644 src/main/java/it/aboutbits/springboot/testing/web/request/PostMultipart.java create mode 100644 src/main/java/it/aboutbits/springboot/testing/web/request/Put.java create mode 100644 src/main/java/it/aboutbits/springboot/testing/web/request/PutMultipart.java create mode 100644 src/main/java/it/aboutbits/springboot/testing/web/request/Request.java create mode 100644 src/main/java/it/aboutbits/springboot/testing/web/request/RequestSecurityAssert.java diff --git a/pom.xml b/pom.xml index e44ddcd..7712b8b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -23,7 +24,7 @@ it.aboutbits spring-boot-toolbox - 1.6.0 + 1.7.0 @@ -39,6 +40,12 @@ spring-boot-starter-validation + + org.springframework.boot + spring-boot-starter-web + + + org.projectlombok @@ -47,6 +54,10 @@ + + org.springframework.security + spring-security-test + org.springframework.boot spring-boot-starter-test diff --git a/src/main/java/it/aboutbits/springboot/testing/web/request/Delete.java b/src/main/java/it/aboutbits/springboot/testing/web/request/Delete.java new file mode 100644 index 0000000..f27bcfe --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/web/request/Delete.java @@ -0,0 +1,30 @@ +package it.aboutbits.springboot.testing.web.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.NonNull; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.springframework.http.MediaType.APPLICATION_JSON; + +public class Delete extends Request { + Delete( + @NonNull MockMvc mockMvc, + @NonNull ObjectMapper objectMapper, + @NonNull String url, + @NonNull Object... pathVariables + ) { + super(mockMvc, objectMapper, url, APPLICATION_JSON, pathVariables); + } + + @Override + protected @NonNull MockHttpServletRequestBuilder getRequestBuilder(@NonNull UrlWithVariables url) { + return MockMvcRequestBuilders.delete(url.url(), url.pathVariables()); + } + + @Override + protected boolean useCsrf() { + return true; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/web/request/Get.java b/src/main/java/it/aboutbits/springboot/testing/web/request/Get.java new file mode 100644 index 0000000..7949d22 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/web/request/Get.java @@ -0,0 +1,30 @@ +package it.aboutbits.springboot.testing.web.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.NonNull; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.springframework.http.MediaType.APPLICATION_JSON; + +public class Get extends Request { + Get( + @NonNull MockMvc mockMvc, + @NonNull ObjectMapper objectMapper, + @NonNull String url, + @NonNull Object... pathVariables + ) { + super(mockMvc, objectMapper, url, APPLICATION_JSON, pathVariables); + } + + @Override + protected @NonNull MockHttpServletRequestBuilder getRequestBuilder(@NonNull UrlWithVariables url) { + return MockMvcRequestBuilders.get(url.url(), url.pathVariables()); + } + + @Override + protected boolean useCsrf() { + return false; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/web/request/HttpTestClient.java b/src/main/java/it/aboutbits/springboot/testing/web/request/HttpTestClient.java new file mode 100644 index 0000000..e6a12c8 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/web/request/HttpTestClient.java @@ -0,0 +1,45 @@ +package it.aboutbits.springboot.testing.web.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +@RequiredArgsConstructor +public class HttpTestClient { + private final MockMvc mockMvc; + private final ObjectMapper objectMapper; + + public Request get(@NonNull String url, Object... pathVariables) { + return new Get(mockMvc, objectMapper, url, pathVariables); + } + + public Request put(@NonNull String url, Object... pathVariables) { + return new Put(mockMvc, objectMapper, url, pathVariables); + } + + public MultipartRequest putMultipart(@NonNull String url, Object... pathVariables) { + return new PutMultipart(mockMvc, objectMapper, url, pathVariables); + } + + public Request post(@NonNull String url, Object... pathVariables) { + return new Post(mockMvc, objectMapper, url, pathVariables); + } + + public MultipartRequest postMultipart(@NonNull String url, Object... pathVariables) { + return new PostMultipart(mockMvc, objectMapper, url, pathVariables); + } + + public Request patch(@NonNull String url, Object... pathVariables) { + return new Patch(mockMvc, objectMapper, url, pathVariables); + } + + public MultipartRequest patchMultipart(@NonNull String url, Object... pathVariables) { + return new PatchMultipart(mockMvc, objectMapper, url, pathVariables); + } + + public Request delete(@NonNull String url, Object... pathVariables) { + return new Delete(mockMvc, objectMapper, url, pathVariables); + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/web/request/HttpTestSecurity.java b/src/main/java/it/aboutbits/springboot/testing/web/request/HttpTestSecurity.java new file mode 100644 index 0000000..f6c4147 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/web/request/HttpTestSecurity.java @@ -0,0 +1,164 @@ +package it.aboutbits.springboot.testing.web.request; + +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import org.springframework.mock.web.MockMultipartFile; + +import static it.aboutbits.springboot.testing.web.request.RequestSecurityAssert.assertThatRequest; + +@Getter +@RequiredArgsConstructor +public class HttpTestSecurity { + private final HttpTestClient httpTestClient; + + public void assertGetGranted(@NonNull String url, Object... pathVariables) { + var result = httpTestClient.get(url, pathVariables) + .returnRaw(); + + assertThatRequest(result).wasGranted(); + } + + public void assertGetDenied(@NonNull String url, Object... pathVariables) { + var result = httpTestClient.get(url, pathVariables) + .returnRaw(); + + assertThatRequest(result).wasDenied(); + } + + public void assertPostGranted(@NonNull String url, Object... pathVariables) { + var result = httpTestClient.post(url, pathVariables) + .body("{}") + .returnRaw(); + + assertThatRequest(result).wasGranted(); + } + + public void assertPostDenied(@NonNull String url, Object... pathVariables) { + var result = httpTestClient.post(url, pathVariables) + .body("{}") + .returnRaw(); + + assertThatRequest(result).wasDenied(); + } + + public void assertPostMultipartGranted( + @NonNull String url, + @NonNull MockMultipartFile multipartFile, + Object... pathVariables + ) { + var result = httpTestClient.postMultipart(url, pathVariables) + .file(multipartFile) + .returnRaw(); + + assertThatRequest(result).wasGranted(); + } + + public void assertPostMultipartDenied( + @NonNull String url, + @NonNull MockMultipartFile multipartFile, + Object... pathVariables + ) { + var result = httpTestClient.postMultipart(url, pathVariables) + .file(multipartFile) + .returnRaw(); + + assertThatRequest(result).wasDenied(); + } + + public void assertPutGranted(@NonNull String url, Object... pathVariables) { + var result = httpTestClient.put(url, pathVariables) + .body("{}") + .returnRaw(); + + assertThatRequest(result).wasGranted(); + } + + public void assertPutDenied(@NonNull String url, Object... pathVariables) { + var result = httpTestClient.put(url, pathVariables) + .body("{}") + .returnRaw(); + + assertThatRequest(result).wasDenied(); + } + + public void assertPutMultipartGranted( + @NonNull String url, + @NonNull MockMultipartFile multipartFile, + Object... pathVariables + ) { + var result = httpTestClient.putMultipart(url, pathVariables) + .file(multipartFile) + .returnRaw(); + + assertThatRequest(result).wasGranted(); + } + + public void assertPutMultipartDenied( + @NonNull String url, + @NonNull MockMultipartFile multipartFile, + Object... pathVariables + ) { + var result = httpTestClient.putMultipart(url, pathVariables) + .file(multipartFile) + .returnRaw(); + + assertThatRequest(result).wasDenied(); + } + + public void assertPatchGranted(@NonNull String url, Object... pathVariables) { + var result = httpTestClient.patch(url, pathVariables) + .body("{}") + .returnRaw(); + + assertThatRequest(result).wasGranted(); + } + + public void assertPatchDenied(@NonNull String url, Object... pathVariables) { + var result = httpTestClient.patch(url, pathVariables) + .body("{}") + .returnRaw(); + + assertThatRequest(result).wasDenied(); + } + + public void assertPatchMultipartGranted( + @NonNull String url, + @NonNull MockMultipartFile multipartFile, + Object... pathVariables + ) { + var result = httpTestClient.patchMultipart(url, pathVariables) + .file(multipartFile) + .returnRaw(); + + assertThatRequest(result).wasGranted(); + } + + public void assertPatchMultipartDenied( + @NonNull String url, + @NonNull MockMultipartFile multipartFile, + Object... pathVariables + ) { + var result = httpTestClient.patchMultipart(url, pathVariables) + .file(multipartFile) + .returnRaw(); + + assertThatRequest(result).wasDenied(); + } + + public void assertDeleteGranted(@NonNull String url, Object... pathVariables) { + var result = httpTestClient.delete(url, pathVariables) + .body("{}") + .returnRaw(); + + assertThatRequest(result).wasGranted(); + } + + public void assertDeleteDenied(@NonNull String url, Object... pathVariables) { + var result = httpTestClient.delete(url, pathVariables) + .body("{}") + .returnRaw(); + + assertThatRequest(result).wasDenied(); + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/web/request/MultipartRequest.java b/src/main/java/it/aboutbits/springboot/testing/web/request/MultipartRequest.java new file mode 100644 index 0000000..a98a413 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/web/request/MultipartRequest.java @@ -0,0 +1,51 @@ +package it.aboutbits.springboot.testing.web.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.NonNull; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.mock.web.MockPart; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; + +import java.util.ArrayList; +import java.util.List; + +import static org.springframework.http.MediaType.MULTIPART_FORM_DATA; + +public abstract class MultipartRequest extends Request { + protected List parts = new ArrayList<>(); + protected List files = new ArrayList<>(); + + MultipartRequest( + @NonNull MockMvc mockMvc, + @NonNull ObjectMapper objectMapper, + @NonNull String url, + @NonNull Object... pathVariables + ) { + super(mockMvc, objectMapper, url, MULTIPART_FORM_DATA, pathVariables); + } + + public MultipartRequest part(@NonNull MockPart part) { + parts.add(part); + return this; + } + + public MultipartRequest file(@NonNull MockMultipartFile file) { + files.add(file); + return this; + } + + @Override + protected @NonNull MockMultipartHttpServletRequestBuilder prepareRequestBuilder() { + var requestBuilder = super.prepareRequestBuilder(); + + for (var file : files) { + requestBuilder.file(file); + } + for (var part : parts) { + requestBuilder.part(part); + } + + return requestBuilder; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/web/request/Patch.java b/src/main/java/it/aboutbits/springboot/testing/web/request/Patch.java new file mode 100644 index 0000000..aad3170 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/web/request/Patch.java @@ -0,0 +1,30 @@ +package it.aboutbits.springboot.testing.web.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.NonNull; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.springframework.http.MediaType.APPLICATION_JSON; + +public class Patch extends Request { + Patch( + @NonNull MockMvc mockMvc, + @NonNull ObjectMapper objectMapper, + @NonNull String url, + @NonNull Object... pathVariables + ) { + super(mockMvc, objectMapper, url, APPLICATION_JSON, pathVariables); + } + + @Override + protected @NonNull MockHttpServletRequestBuilder getRequestBuilder(@NonNull UrlWithVariables url) { + return MockMvcRequestBuilders.patch(url.url(), url.pathVariables()); + } + + @Override + protected boolean useCsrf() { + return true; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/web/request/PatchMultipart.java b/src/main/java/it/aboutbits/springboot/testing/web/request/PatchMultipart.java new file mode 100644 index 0000000..d33917e --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/web/request/PatchMultipart.java @@ -0,0 +1,29 @@ +package it.aboutbits.springboot.testing.web.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.NonNull; +import org.springframework.http.HttpMethod; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +public class PatchMultipart extends MultipartRequest { + PatchMultipart( + @NonNull MockMvc mockMvc, + @NonNull ObjectMapper objectMapper, + @NonNull String url, + @NonNull Object... pathVariables + ) { + super(mockMvc, objectMapper, url, pathVariables); + } + + @Override + protected @NonNull MockMultipartHttpServletRequestBuilder getRequestBuilder(@NonNull UrlWithVariables url) { + return MockMvcRequestBuilders.multipart(HttpMethod.PATCH, url.url(), url.pathVariables()); + } + + @Override + protected boolean useCsrf() { + return true; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/web/request/Post.java b/src/main/java/it/aboutbits/springboot/testing/web/request/Post.java new file mode 100644 index 0000000..767927c --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/web/request/Post.java @@ -0,0 +1,30 @@ +package it.aboutbits.springboot.testing.web.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.NonNull; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.springframework.http.MediaType.APPLICATION_JSON; + +public class Post extends Request { + Post( + @NonNull MockMvc mockMvc, + @NonNull ObjectMapper objectMapper, + @NonNull String url, + @NonNull Object... pathVariables + ) { + super(mockMvc, objectMapper, url, APPLICATION_JSON, pathVariables); + } + + @Override + protected @NonNull MockHttpServletRequestBuilder getRequestBuilder(@NonNull UrlWithVariables url) { + return MockMvcRequestBuilders.post(url.url(), url.pathVariables()); + } + + @Override + protected boolean useCsrf() { + return true; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/web/request/PostMultipart.java b/src/main/java/it/aboutbits/springboot/testing/web/request/PostMultipart.java new file mode 100644 index 0000000..eb49589 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/web/request/PostMultipart.java @@ -0,0 +1,29 @@ +package it.aboutbits.springboot.testing.web.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.NonNull; +import org.springframework.http.HttpMethod; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +public class PostMultipart extends MultipartRequest { + PostMultipart( + @NonNull MockMvc mockMvc, + @NonNull ObjectMapper objectMapper, + @NonNull String url, + @NonNull Object... pathVariables + ) { + super(mockMvc, objectMapper, url, pathVariables); + } + + @Override + protected @NonNull MockMultipartHttpServletRequestBuilder getRequestBuilder(@NonNull UrlWithVariables url) { + return MockMvcRequestBuilders.multipart(HttpMethod.POST, url.url(), url.pathVariables()); + } + + @Override + protected boolean useCsrf() { + return true; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/web/request/Put.java b/src/main/java/it/aboutbits/springboot/testing/web/request/Put.java new file mode 100644 index 0000000..a076509 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/web/request/Put.java @@ -0,0 +1,30 @@ +package it.aboutbits.springboot.testing.web.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.NonNull; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static org.springframework.http.MediaType.APPLICATION_JSON; + +public class Put extends Request { + Put( + @NonNull MockMvc mockMvc, + @NonNull ObjectMapper objectMapper, + @NonNull String url, + @NonNull Object... pathVariables + ) { + super(mockMvc, objectMapper, url, APPLICATION_JSON, pathVariables); + } + + @Override + protected @NonNull MockHttpServletRequestBuilder getRequestBuilder(@NonNull UrlWithVariables url) { + return MockMvcRequestBuilders.put(url.url(), url.pathVariables()); + } + + @Override + protected boolean useCsrf() { + return true; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/web/request/PutMultipart.java b/src/main/java/it/aboutbits/springboot/testing/web/request/PutMultipart.java new file mode 100644 index 0000000..05255db --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/web/request/PutMultipart.java @@ -0,0 +1,29 @@ +package it.aboutbits.springboot.testing.web.request; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.NonNull; +import org.springframework.http.HttpMethod; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMultipartHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +public class PutMultipart extends MultipartRequest { + PutMultipart( + @NonNull MockMvc mockMvc, + @NonNull ObjectMapper objectMapper, + @NonNull String url, + @NonNull Object... pathVariables + ) { + super(mockMvc, objectMapper, url, pathVariables); + } + + @Override + protected @NonNull MockMultipartHttpServletRequestBuilder getRequestBuilder(@NonNull UrlWithVariables url) { + return MockMvcRequestBuilders.multipart(HttpMethod.PUT, url.url(), url.pathVariables()); + } + + @Override + protected boolean useCsrf() { + return true; + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/web/request/Request.java b/src/main/java/it/aboutbits/springboot/testing/web/request/Request.java new file mode 100644 index 0000000..8221147 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/web/request/Request.java @@ -0,0 +1,274 @@ +package it.aboutbits.springboot.testing.web.request; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import it.aboutbits.springboot.toolbox.web.response.ErrorResponse; +import it.aboutbits.springboot.toolbox.web.response.ItemResponse; +import it.aboutbits.springboot.toolbox.web.response.ItemResponseWithMeta; +import it.aboutbits.springboot.toolbox.web.response.ListResponse; +import it.aboutbits.springboot.toolbox.web.response.PagedResponse; +import it.aboutbits.springboot.toolbox.web.response.meta.Meta; +import jakarta.servlet.http.Cookie; +import lombok.NonNull; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; + +@Slf4j +public abstract class Request { + protected record UsernameAndPassword(String username, String password) { + } + + protected record UrlWithVariables(String url, Object... pathVariables) { + } + + protected final ObjectMapper objectMapper; + protected final MockMvc mockMvc; + protected final UrlWithVariables url; + protected final MediaType contentType; + + protected String body = ""; + protected HttpStatus status = null; + protected Cookie[] cookies = new Cookie[0]; + protected UsernameAndPassword basicAuth = null; + protected Map parameters = new HashMap<>(); + protected Map parametersArray = new HashMap<>(); + + Request( + @NonNull MockMvc mockMvc, + @NonNull ObjectMapper objectMapper, + @NonNull String url, + @NonNull MediaType contentType, + @NonNull Object... pathVariables + ) { + this.objectMapper = objectMapper; + + this.mockMvc = mockMvc; + this.url = new UrlWithVariables(url, pathVariables); + this.contentType = contentType; + } + + public Request cookies(@NonNull Cookie... cookies) { + this.cookies = cookies; + return this; + } + + public Request body(@NonNull Object body) { + if (body instanceof String stringBody) { + this.body = stringBody; + return this; + } + + try { + this.body = objectMapper.writeValueAsString(body); + } catch (JsonProcessingException e) { + log.error("Failed to write body as JSON.", e); + throw new RuntimeException(e); + } + return this; + } + + public Request param(@NonNull String name, Object value) { + this.parameters.put(name, String.valueOf(value)); + return this; + } + + public Request param(@NonNull String name, Collection values) { + this.parametersArray.put( + name, + values.stream() + .map(String::valueOf) + .toArray(String[]::new) + ); + return this; + } + + public Request expectStatus(@NonNull HttpStatus status) { + this.status = status; + return this; + } + + public Request basicAuth(@NonNull String username, @NonNull String password) { + this.basicAuth = new UsernameAndPassword(username, password); + return this; + } + + @SneakyThrows(UnsupportedEncodingException.class) + public ItemResponse returnItem(@NonNull Class clazz) { + var res = _execute(); + + var resString = res.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8); + + var typeFactory = objectMapper.getTypeFactory(); + var javaType = typeFactory.constructParametricType(ItemResponse.class, clazz); + + try { + return objectMapper.readValue(resString, javaType); + } catch (JsonProcessingException e) { + log.error("Failed to read response as JSON.", e); + throw new RuntimeException(e); + } + } + + @SneakyThrows(UnsupportedEncodingException.class) + public ItemResponseWithMeta returnItem( + @NonNull Class clazz, + @NonNull Class metaClass + ) { + var res = _execute(); + + var resString = res.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8); + + var typeFactory = objectMapper.getTypeFactory(); + var javaType = typeFactory.constructParametricType(ItemResponseWithMeta.class, clazz, metaClass); + + try { + return objectMapper.readValue(resString, javaType); + } catch (JsonProcessingException e) { + log.error("Failed to read response as JSON.", e); + throw new RuntimeException(e); + } + } + + @SneakyThrows(UnsupportedEncodingException.class) + public ListResponse returnList(@NonNull Class clazz) { + var res = _execute(); + + var resString = res.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8); + + var typeFactory = objectMapper.getTypeFactory(); + var javaType = typeFactory.constructParametricType(ListResponse.class, clazz); + + try { + return objectMapper.readValue(resString, javaType); + } catch (JsonProcessingException e) { + log.error("Failed to read response as JSON.", e); + throw new RuntimeException(e); + } + } + + @SneakyThrows(UnsupportedEncodingException.class) + public PagedResponse returnPage(@NonNull Class clazz) { + var res = _execute(); + + var resString = res.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8); + + var typeFactory = objectMapper.getTypeFactory(); + var javaType = typeFactory.constructParametricType(PagedResponse.class, clazz); + + try { + return objectMapper.readValue(resString, javaType); + } catch (JsonProcessingException e) { + log.error("Failed to read response as JSON.", e); + throw new RuntimeException(e); + } + } + + @SneakyThrows(UnsupportedEncodingException.class) + public ErrorResponse returnError() { + var res = _execute(); + + var resString = res.andReturn().getResponse().getContentAsString(StandardCharsets.UTF_8); + + try { + return objectMapper.readValue(resString, ErrorResponse.class); + } catch (JsonProcessingException e) { + log.error("Failed to read response as JSON.", e); + throw new RuntimeException(e); + } + } + + public ResultActions returnRaw() { + return _execute(); + } + + public void execute() { + _execute(); + } + + @SuppressWarnings({"checkstyle:MethodName", "java:S100"}) + private ResultActions _execute() { + var requestBuilder = prepareRequestBuilder(); + + ResultActions resultActions = null; + try { + resultActions = mockMvc.perform(requestBuilder); + } catch (Exception e) { + log.error("Request execution failed.", e); + throw new RuntimeException(e); + } + + maybeCheckStatus(resultActions); + + return resultActions; + } + + @NonNull + @SuppressWarnings("unchecked") + protected R prepareRequestBuilder() { + var requestBuilder = getRequestBuilder(url) + .content(body) + .contentType(contentType); + + if (useCsrf()) { + requestBuilder.with(csrf().asHeader()); + } + + if (basicAuth != null) { + requestBuilder.with( + httpBasic( + basicAuth.username(), + basicAuth.password() + ) + ); + } + + if (cookies.length > 0) { + requestBuilder.cookie(cookies); + } + + for (var entry : parameters.entrySet()) { + requestBuilder.param(entry.getKey(), entry.getValue()); + } + + for (var entry : parametersArray.entrySet()) { + requestBuilder.param(entry.getKey(), entry.getValue()); + } + + return (R) requestBuilder; + } + + @NonNull + protected abstract R getRequestBuilder(@NonNull UrlWithVariables url); + + protected abstract boolean useCsrf(); + + protected void maybeCheckStatus(@NonNull ResultActions resultActions) { + var mvcResult = resultActions.andReturn(); + if (status != null) { + assertThat( + mvcResult.getResponse().getStatus() + ) + .withFailMessage( + "Expected status '%s' but was '%s'", + status.value(), + mvcResult.getResponse().getStatus() + ) + .isEqualTo(status.value()); + } + } +} diff --git a/src/main/java/it/aboutbits/springboot/testing/web/request/RequestSecurityAssert.java b/src/main/java/it/aboutbits/springboot/testing/web/request/RequestSecurityAssert.java new file mode 100644 index 0000000..742f069 --- /dev/null +++ b/src/main/java/it/aboutbits/springboot/testing/web/request/RequestSecurityAssert.java @@ -0,0 +1,32 @@ +package it.aboutbits.springboot.testing.web.request; + +import org.assertj.core.api.AbstractAssert; +import org.springframework.http.HttpStatus; +import org.springframework.test.web.servlet.ResultActions; + +import static org.assertj.core.api.Assertions.assertThat; + +public final class RequestSecurityAssert extends AbstractAssert { + RequestSecurityAssert(ResultActions actual) { + super(actual, RequestSecurityAssert.class); + } + + public static RequestSecurityAssert assertThatRequest(ResultActions actual) { + return new RequestSecurityAssert(actual); + } + + public void wasUnauthorized() { + var status = actual.andReturn().getResponse().getStatus(); + assertThat(status).isEqualTo(HttpStatus.UNAUTHORIZED.value()); + } + + public void wasDenied() { + var status = actual.andReturn().getResponse().getStatus(); + assertThat(status).isEqualTo(HttpStatus.FORBIDDEN.value()); + } + + public void wasGranted() { + var status = actual.andReturn().getResponse().getStatus(); + assertThat(status).isNotEqualTo(HttpStatus.FORBIDDEN.value()); + } +}