Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,6 @@ jacocoTestReport {
fileTree(dir: it, exclude: [\
'edu/kit/datamanager/pit/configuration/**', \
'edu/kit/datamanager/pit/web/converter/**', \
'edu/kit/datamanager/pit/web/ExtendedErrorAttributes**', \
'edu/kit/datamanager/pit/web/UncontrolledExceptionHandler**', \
'edu/kit/datamanager/pit/common/**', \
'edu/kit/datamanager/pit/Application*'
Expand Down
2 changes: 1 addition & 1 deletion docker/test_docker.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ docker run -p 8090:8090 --detach --name $container $tag
#####################################
### tests ###########################
#####################################
hurl --retry=10 --retry-interval=10000 \
hurl --retry 100 --retry-interval 1s \
--test "$docker_dir"/tests/*.hurl
failure=$?
#####################################
Expand Down
23 changes: 23 additions & 0 deletions docker/tests/create_fail_error_message.hurl
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# create/register a pid record with missing required fields
POST http://localhost:8090/api/v1/pit/pid/
Content-Type: application/vnd.datamanager.pid.simple+json
Accept: application/vnd.datamanager.pid.simple+json
{
"record": [
{ "key": "21.T11148/076759916209e5d62bd5", "value": "21.T11148/301c6f04763a16f0f72a" }
]
}

# this will fail because the profile validation will fail
HTTP 400
# in the response body, you'll get the record and the pid
[Asserts]
jsonpath "$.detail" exists
jsonpath "$.detail" contains "Missing mandatory types"
jsonpath "$.pid-record" exists

# Lets ask if the record exists now:
HEAD http://localhost:8090/api/v1/pit/pid/{{pid}}

# it should not:
HTTP 404
6 changes: 6 additions & 0 deletions src/main/java/edu/kit/datamanager/pit/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import edu.kit.datamanager.pit.pitservice.impl.TypingService;
import edu.kit.datamanager.pit.typeregistry.ITypeRegistry;
import edu.kit.datamanager.pit.typeregistry.impl.TypeRegistry;
import edu.kit.datamanager.pit.web.ExtendedErrorAttributes;
import edu.kit.datamanager.pit.web.converter.SimplePidRecordConverter;
import edu.kit.datamanager.security.filter.KeycloakJwtProperties;

Expand Down Expand Up @@ -100,6 +101,11 @@ public Logger logger(InjectionPoint injectionPoint) {
return LoggerFactory.getLogger(targetClass.getCanonicalName());
}

@Bean
public ExtendedErrorAttributes errorAttributes(ObjectMapper objectMapper) {
return new ExtendedErrorAttributes(objectMapper);
}

@Bean
public ITypeRegistry typeRegistry() {
return new TypeRegistry();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@

import edu.kit.datamanager.pit.domain.PIDRecord;

import java.io.Serial;

/**
* Indicates that a PID was given which could not be resolved to answer the
* request properly.
*/
public class RecordValidationException extends ResponseStatusException {

private static final String VALIDATION_OF_RECORD = "Validation of record ";
private static final long serialVersionUID = 1L;
@Serial
private static final long serialVersionUID = -7287999233733933282L;
private static final HttpStatus HTTP_STATUS = HttpStatus.BAD_REQUEST;

// For cases in which the PID record shold be appended to the error response.
private final transient PIDRecord pidRecord;
// For cases in which the PID record should be appended to the error response.
private final PIDRecord pidRecord;

public RecordValidationException(PIDRecord pidRecord) {
super(HTTP_STATUS, VALIDATION_OF_RECORD + pidRecord.getPid() + " failed.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,29 @@

import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;

import com.fasterxml.jackson.databind.ObjectMapper;

import edu.kit.datamanager.pit.common.RecordValidationException;

@Component
public class ExtendedErrorAttributes extends DefaultErrorAttributes {

@Autowired(required = true)
ObjectMapper objectMapperBean;

public ExtendedErrorAttributes(ObjectMapper objectMapperBean) {
this.objectMapperBean = objectMapperBean;
}

@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
final Map<String, Object> errorAttributes =
super.getErrorAttributes(webRequest, options);

final Throwable error = super.getError(webRequest);
if (error instanceof RecordValidationException) {
final RecordValidationException validationError = (RecordValidationException) error;
if (error instanceof RecordValidationException validationError) {
try {
errorAttributes.put("pid-record", objectMapperBean.writeValueAsString(validationError.getPidRecord()));
} catch (Exception e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import jakarta.annotation.Nonnull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpInputMessage;
Expand All @@ -24,21 +24,21 @@
/**
* Converts to-and-from PIDRecord format when SimplePidRecord format is actually
* expected.
*
* <p>
* Do not use explicitly. Spring will use it, as explained below.
*
* <p>
* The idea is that all handlers in the REST API simply expect a serialized
* (marshalled) version of a PID record. This is not always true, though. The
* PIDRecord representation is pretty complex. To offer a simpler format,
* `SimplePidRecord` was introduced. To avoid larger modifications within the
* code, and to not break the API, the simple format comes into play only when
* its content type is being used.
*
* <p>
* If the client wants to send in the simple format, it needs to set the
* content-type header accordingly. Spring will then use this converter to
* convert it directly into a PIDRecord instance, as the handler expects. This
* way only one handler must be used for multiple formats.
*
* <p>
* For accepting formats, it is the same. With the accept header, a client may
* control which format it would like to receive. If it prefers to receive the
* simple format and sets the header accordingly, instead of directly
Expand All @@ -54,29 +54,29 @@ private boolean isValidMediaType(MediaType arg1) {
}

@Override
public boolean canRead(Class<?> arg0, MediaType arg1) {
if (arg0 == null || arg1 == null) {
public boolean canRead(@Nonnull Class<?> arg0, MediaType arg1) {
if (arg1 == null) {
return false;
}
LOGGER.trace("canRead: Checking applicability for class {} and mediatype {}.", arg0, arg1);
return PIDRecord.class.equals(arg0) && isValidMediaType(arg1);
}

@Override
public boolean canWrite(Class<?> arg0, MediaType arg1) {
public boolean canWrite(@Nonnull Class<?> arg0, MediaType arg1) {
LOGGER.trace("canWrite: Checking applicability for class {} and mediatype {}.", arg0, arg1);
return PIDRecord.class.equals(arg0) && isValidMediaType(arg1);
}

@Override
public List<MediaType> getSupportedMediaTypes() {
return Arrays.asList(
public @Nonnull List<MediaType> getSupportedMediaTypes() {
return List.of(
MediaType.valueOf(SimplePidRecord.CONTENT_TYPE)
);
}

@Override
public PIDRecord read(Class<? extends PIDRecord> arg0, HttpInputMessage arg1)
public @Nonnull PIDRecord read(@Nonnull Class<? extends PIDRecord> arg0, @Nonnull HttpInputMessage arg1)
throws IOException, HttpMessageNotReadableException {
LOGGER.trace("Read simple message from client and convert to PIDRecord.");
try (InputStreamReader reader = new InputStreamReader(arg1.getBody(), StandardCharsets.UTF_8)) {
Expand All @@ -86,7 +86,7 @@ public PIDRecord read(Class<? extends PIDRecord> arg0, HttpInputMessage arg1)
}

@Override
public void write(PIDRecord arg0, MediaType arg1, HttpOutputMessage arg2)
public void write(@Nonnull PIDRecord arg0, MediaType arg1, HttpOutputMessage arg2)
throws IOException, HttpMessageNotWritableException {
LOGGER.trace("Write PIDRecord to simple format for client.");
SimplePidRecord sim = new SimplePidRecord(arg0);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package edu.kit.datamanager.pit.web;

import java.util.Map;

import edu.kit.datamanager.pit.common.RecordValidationException;
import edu.kit.datamanager.pit.domain.PIDRecord;
import jakarta.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.context.request.ServletWebRequest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
@ActiveProfiles("test")
public class ExtendedErrorAttributesTest {

@Autowired
private TestRestTemplate restTemplate;

@Autowired
private WebApplicationContext webApplicationContext;

@Autowired
private ExtendedErrorAttributes errorAttributes;

@Test
public void testPidRecordPresence() {
// Create a mock request to pass to the error attributes
HttpServletRequest request = new MockHttpServletRequest();
WebRequest webRequest = new ServletWebRequest(request);

// Simulate an exception to be handled by the error attributes
request.setAttribute("jakarta.servlet.error.exception", new RecordValidationException(
new PIDRecord().withPID("asdfg"),
"Validation failed"
));

// Get the error attributes
Map<String, Object> attributes = errorAttributes.getErrorAttributes(webRequest, ErrorAttributeOptions.defaults());

// Check if the custom attribute is present
assertTrue(attributes.containsKey("pid-record"));
}

@Test
public void testBeanRegistration() {
// Check if the ExtendedErrorAttributes bean is registered
ExtendedErrorAttributes extendedErrorAttributes = webApplicationContext.getBean(ExtendedErrorAttributes.class);
assertNotNull(extendedErrorAttributes, "ExtendedErrorAttributes bean should be registered");
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
package edu.kit.datamanager.pit.web;

import static org.junit.jupiter.api.Assertions.assertTrue;

import com.fasterxml.jackson.databind.ObjectMapper;
import edu.kit.datamanager.pit.domain.PIDRecord;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
Expand All @@ -18,7 +21,9 @@
import edu.kit.datamanager.pit.SpringTestHelper;
import edu.kit.datamanager.pit.pitservice.impl.NoValidationStrategy;

import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;

// Might be needed for WebApp testing according to https://www.baeldung.com/integration-testing-in-spring
//@WebAppConfiguration
Expand Down Expand Up @@ -46,6 +51,25 @@ void setup() throws Exception {
.assertNoBeanInstanceOf(NoValidationStrategy.class);
}

@Test
void testPidRecordInErrorJson() throws Exception {
PIDRecord r = new PIDRecord();
r.addEntry("21.T11148/076759916209e5d62bd5", "for Testing", "21.T11148/301c6f04763a16f0f72a");
MvcResult result = this.mockMvc
.perform(
post("/api/v1/pit/pid/")
.contentType(MediaType.APPLICATION_JSON)
.characterEncoding("utf-8")
.content(new ObjectMapper().writeValueAsString(r))
.accept(MediaType.ALL)
)
.andDo(MockMvcResultHandlers.print())
.andExpect(MockMvcResultMatchers.status().isBadRequest())
.andExpect(MockMvcResultMatchers.jsonPath("$.pid-record", Matchers.containsString("for Testing")))
.andReturn();
assertFalse(result.getResponse().getContentAsString().isEmpty());
}

/**
* Tests if the swagger ui and openapi definition is accessible.
*
Expand Down
Loading