Skip to content
Merged
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
2 changes: 1 addition & 1 deletion NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* Description ([ISSUE](https://folio-org.atlassian.net/browse/ISSUE))

### Bug fixes
* Description ([ISSUE](https://folio-org.atlassian.net/browse/ISSUE))
* Change the "500" status code to "400" when the request has invalid UUID ([MRSPECS-52](https://folio-org.atlassian.net/browse/MRSPECS-52))

### Tech Dept
* Add integration test to cover sync endpoint for authority ([MRSPECS-105](https://folio-org.atlassian.net/browse/MRSPECS-105))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package org.folio.rspec.controller.handler;

import static org.folio.rspec.domain.dto.ErrorCode.INVALID_QUERY_ENUM_VALUE;
import static org.folio.rspec.domain.dto.ErrorCode.INVALID_QUERY_UUID_VALUE;

import java.util.Arrays;
import java.util.UUID;
import java.util.regex.Pattern;
import lombok.RequiredArgsConstructor;
import org.folio.rspec.domain.dto.Error;
Expand Down Expand Up @@ -30,32 +32,50 @@ public class MethodArgumentTypeMismatchExceptionHandler implements ServiceExcept
public ResponseEntity<ErrorCollection> handleException(Exception e) {
var exception = (MethodArgumentTypeMismatchException) e;
var requiredType = exception.getRequiredType();
var errorCollection = new ErrorCollection();

if (requiredType != null && requiredType.isEnum()) {
errorCollection.addErrorsItem(buildEnumError(exception, requiredType));
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_CONTENT).body(errorCollection);
} else {
errorCollection.addErrorsItem(buildUnexpectedError(exception));
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorCollection);
return buildResponse(HttpStatus.UNPROCESSABLE_CONTENT, buildEnumError(exception, requiredType));
}
if (requiredType != null && requiredType.isAssignableFrom(UUID.class)) {
return buildResponse(HttpStatus.BAD_REQUEST, buildUuidError(exception));
}
return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, buildUnexpectedError(exception));
}

@Override
public boolean canHandle(Exception e) {
return e instanceof MethodArgumentTypeMismatchException;
}

private ResponseEntity<ErrorCollection> buildResponse(HttpStatus status, Error error) {
return ResponseEntity.status(status).body(new ErrorCollection().addErrorsItem(error));
}

private Error buildEnumError(MethodArgumentTypeMismatchException e, Class<?> requiredType) {
var message = buildErrorMessage(e, requiredType.getEnumConstants());
var message = buildEnumErrorMessage(e, requiredType.getEnumConstants());
return new Error()
.message(message)
.code(INVALID_QUERY_ENUM_VALUE.getCode())
.type(INVALID_QUERY_ENUM_VALUE.getType())
.addParametersItem(new Parameter().key(e.getName()).value(String.valueOf(e.getValue())));
}

private String buildErrorMessage(MethodArgumentTypeMismatchException e, Object[] enumConstants) {
private Error buildUuidError(MethodArgumentTypeMismatchException e) {
var inputValue = String.valueOf(e.getValue());
var message = buildUuidErrorMessage(inputValue);
return new Error()
.message(message)
.code(INVALID_QUERY_UUID_VALUE.getCode())
.type(INVALID_QUERY_UUID_VALUE.getType())
.addParametersItem(new Parameter().key(e.getName()).value(inputValue));
}

private String buildUuidErrorMessage(String inputValue) {
return translationService.format(INVALID_QUERY_UUID_VALUE.getMessageKey(),
INVALID_VALUE_MSG_ARG, inputValue);
}

private String buildEnumErrorMessage(MethodArgumentTypeMismatchException e, Object[] enumConstants) {
return translationService.format(INVALID_QUERY_ENUM_VALUE.getMessageKey(),
INVALID_VALUE_MSG_ARG, getInvalidValue(e.getRootCause()),
POSSIBLE_VALUES_MSG_ARG, translationService.formatList(Arrays.asList(enumConstants)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public enum ErrorCode {

INVALID_QUERY_VALUE("invalid-query-value", "101", null),
INVALID_QUERY_ENUM_VALUE("invalid-query-enum-value", "102", "invalid.request.query-param.enum"),
INVALID_QUERY_UUID_VALUE("invalid-uuid-value", "111", "invalid.request.query-param.uuid"),
INVALID_REQUEST_PARAMETER("invalid-request-parameter", "103", null),
DUPLICATE_FIELD_TAG("duplicate-specification-field-tag", "104", "specification.field.tag.duplicate"),
SPECIFICATION_FETCH_FAILED("specification-fetch-failed", "105", "specification.fetch.failed"),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.folio.rspec.controller;

import static org.folio.support.ApiEndpoints.SPECIFICATION_PATH;
import static org.folio.support.ApiEndpoints.specificationFieldsPath;
import static org.folio.support.ApiEndpoints.specificationPath;
import static org.folio.support.ApiEndpoints.specificationRulePath;
Expand Down Expand Up @@ -164,6 +165,20 @@ void getSpecification_returnSpecification(@Random UUID specificationId,
verify(specificationService).getSpecificationById(specificationId, IncludeParam.NONE);
}

@Test
void getSpecification_negative_invalidUuid() throws Exception {
var specificationId = "invalid-uuid";

var requestBuilder = get(SPECIFICATION_PATH.formatted(specificationId))
.contentType(APPLICATION_JSON);

mockMvc.perform(requestBuilder)
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors.size()", is(1)))
.andExpect(jsonPath("$.errors.[*].message", hasItem(is("Invalid value [%s]. Must be a valid UUID."
.formatted(specificationId)))));
}

@Test
void getSpecificationRules_returnSpecificationRules(@Random SpecificationRuleDto ruleDto) throws Exception {
var specificationId = UUID.randomUUID();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;

import java.util.UUID;
import org.folio.rspec.domain.dto.Error;
import org.folio.rspec.service.i18n.ExtendedTranslationService;
import org.folio.spring.testing.type.UnitTest;
Expand Down Expand Up @@ -63,6 +64,29 @@ void testHandleException_unexpectedEnumValue() {
.containsExactly(expectedMessage, "invalid-query-enum-value", "102", "arg1", "invalidValue");
}

@Test
void testHandleException_invalidUuidValue() {
var expectedMessage = "error message";
var handlerClass = UUID.class;
var invalidValue = "not-a-uuid";

when(exception.getName()).thenReturn("arg1");
when(exception.getRequiredType()).thenAnswer(invocation -> handlerClass);
when(exception.getValue()).thenReturn(invalidValue);
when(translationService.format(anyString(), eq("invalidValue"), eq(invalidValue)))
.thenReturn(expectedMessage);

var response = handler.handleException(exception);

assertThat(response.getStatusCode().value()).isEqualTo(HttpStatus.BAD_REQUEST.value());
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getErrors()).isNotNull().hasSize(1);
assertThat(response.getBody().getErrors().getFirst())
.extracting(Error::getMessage, Error::getType, Error::getCode, error -> error.getParameters().getFirst().getKey(),
error -> error.getParameters().getFirst().getValue())
.containsExactly(expectedMessage, "invalid-uuid-value", "111", "arg1", invalidValue);
}

@Test
void testHandleException_unexpectedError() {
var handlerClass = MethodArgumentTypeMismatchExceptionHandler.class;
Expand Down
1 change: 1 addition & 0 deletions translations/mod-record-specifications/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"control-field.resource.not-allowed": "Cannot define {parameter}s for 00X control fields.",

"invalid.request.query-param.enum": "Unexpected value [{invalidValue}]. Possible values: [{possibleValues}].",
"invalid.request.query-param.uuid": "Invalid value [{invalidValue}]. Must be a valid UUID.",
"Pattern.SpecificationFieldChangeDto.tag": "A ''{parameter}'' field must contain three characters and can only accept numbers 0-9.",
"Pattern.IndicatorCodeChangeDto.code": "A ''{parameter}'' field must contain one character and can only accept numbers 0-9, letters a-z or a '#'.",
"Pattern.SubfieldChangeDto.code": "A ''{parameter}'' field must contain one character and can only accept numbers 0-9 or letters a-z.",
Expand Down
Loading