Commit 937ce44b authored by femiadeyemi's avatar femiadeyemi
Browse files

organisations-api: add integration tests

Motivation:

The organisations api can handle varieties of request. Since
this will evolve over time, integration tests are needed to
ensure that basic functionalities are not broken when a new
patch is submitted.

Modification:

- add various tests scenarios to cover all HTTP methods in the
    organisations API
- adjust the organisations controller:
    - post method to return 201, location url in the header
        and a json body that contain the newly create organisation
    - put method to return 200 if the uuid already exist or 201
        if it is a new uuid.
    - get, put and patch will first check the validity of the uuid
        and thrown and exception if the uuid is invalid.
    - change the return type of deleteUuid from void to long
- create CerebrumInvalidUuidException which will be use for invalid
    uuid
- add two exceptions to the CerebrumExceptionHandler which are
    CerebrumInvalidUuidException and HttpMessageNotReadableException
    (this is for malformed json or json+patch)
- add tests to check that CerebrumExceptionHandler handle those two
    new added exceptions
- fix some minor bugs inside CerebrumEntityUuidGenerator
- obtain and set aai token variable that will be use by the ci

Result:

Improve test coverage for organisation API and some minor
bug fixes.

Target: master
Acked-by: Franz Stephan
Review-at: https://gitlab.hzdr.de/hifis-technical-platform/helmholtz-cerebrum/-/merge_requests/20
parent 66b56ec5
Pipeline #39078 failed with stages
in 7 minutes and 57 seconds
......@@ -5,4 +5,6 @@ ci_build:
- merge_requests
stage: build
image: maven:3-jdk-11
script: "mvn -B package --file pom.xml"
script:
- 'export AAI_TOKEN=$(curl -u "helmholtz-marketplace:${CLIENT_SECRET}" -X POST "https://login.helmholtz.de/oauth2/token" -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=refresh_token&refresh_token=${AAI_REFRESH_TOKEN}&client_id=helmholtz-marketplace&client_secret=${CLIENT_SECRET}" | sed "s/{.*\"access_token\":\"\([^\"]*\).*}/\1/g")'
- "mvn -Dtoken=$AAI_TOKEN -B package --file pom.xml"
\ No newline at end of file
......@@ -7,7 +7,9 @@ maven_build:
dependencies:
- ci_build
image: maven:3-jdk-11
script: "mvn install"
script:
- 'export AAI_TOKEN=$(curl -u "helmholtz-marketplace:${CLIENT_SECRET}" -X POST "https://login.helmholtz.de/oauth2/token" -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=refresh_token&refresh_token=${AAI_REFRESH_TOKEN}&client_id=helmholtz-marketplace&client_secret=${CLIENT_SECRET}" | sed "s/{.*\"access_token\":\"\([^\"]*\).*}/\1/g")'
- "mvn -Dtoken=$AAI_TOKEN install"
artifacts:
paths:
- "target/*.jar"
......
run_sonar_test:
stage: test_sonar
image: maven:3-jdk-11
script: "mvn -P static-code-analysis clean verify sonar:sonar"
script:
- 'export AAI_TOKEN=$(curl -u "helmholtz-marketplace:${CLIENT_SECRET}" -X POST "https://login.helmholtz.de/oauth2/token" -H "Content-Type: application/x-www-form-urlencoded" -d "grant_type=refresh_token&refresh_token=${AAI_REFRESH_TOKEN}&client_id=helmholtz-marketplace&client_secret=${CLIENT_SECRET}" | sed "s/{.*\"access_token\":\"\([^\"]*\).*}/\1/g")'
- "mvn -Dtoken=$AAI_TOKEN -P static-code-analysis clean verify sonar:sonar"
......@@ -3,7 +3,9 @@ package de.helmholtz.marketplace.cerebrum.controller;
import de.helmholtz.marketplace.cerebrum.entities.Organization;
import de.helmholtz.marketplace.cerebrum.errorhandling.CerebrumApiError;
import de.helmholtz.marketplace.cerebrum.errorhandling.exception.CerebrumEntityNotFoundException;
import de.helmholtz.marketplace.cerebrum.errorhandling.exception.CerebrumInvalidUuidException;
import de.helmholtz.marketplace.cerebrum.repository.OrganizationRepository;
import de.helmholtz.marketplace.cerebrum.utils.CerebrumEntityUuidGenerator;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
......@@ -25,6 +27,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
......@@ -36,12 +39,18 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import javax.validation.Valid;
import javax.validation.constraints.Min;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@RestController
@Validated
@RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE,
path = "${spring.data.rest.base-path}/organizations")
@Tag(name = "organizations", description = "The Organization API")
......@@ -66,9 +75,9 @@ public class OrganizationController {
@GetMapping(path = "")
public Iterable<Organization> getOrganizations(
@Parameter(description = "specify the page number")
@RequestParam(value = "page", defaultValue = "0") Integer page,
@RequestParam(value = "page", defaultValue = "0") @Min(0) Integer page,
@Parameter(description = "limit the number of records returned in one page")
@RequestParam(value = "size", defaultValue = "20") Integer size,
@RequestParam(value = "size", defaultValue = "20") @Min(1) Integer size,
@Parameter(description = "sort the fetched data in either ascending (asc) " +
"or descending (desc) according to one or more of the organisation " +
"properties. Eg. to sort the list in ascending order base on the " +
......@@ -97,8 +106,11 @@ public class OrganizationController {
@Parameter(description = "ID of the organization that needs to be fetched")
@PathVariable(name = "uuid") String uuid)
{
return organizationRepository.findByUuid(uuid)
.orElseThrow(() -> new CerebrumEntityNotFoundException("organization", uuid));
if (Boolean.TRUE.equals(CerebrumEntityUuidGenerator.isValid(uuid))) {
return organizationRepository.findByUuid(uuid)
.orElseThrow(() -> new CerebrumEntityNotFoundException("organization", uuid));
}
else throw new CerebrumInvalidUuidException(uuid);
}
/* create Organization */
......@@ -113,11 +125,19 @@ public class OrganizationController {
@ApiResponse(responseCode = "401", description = "unauthorised", content = @Content())
})
@PostMapping(path = "", consumes = MediaType.APPLICATION_JSON_VALUE)
public Organization createOrganization(
public ResponseEntity<Organization> createOrganization(
@Parameter(description = "organization object that needs to be added to the marketplace",
required = true, schema = @Schema(implementation = Organization.class))
@Valid @RequestBody Organization organization) {
return organizationRepository.save(organization);
@Valid @RequestBody Organization organization, UriComponentsBuilder uriComponentsBuilder)
{
Organization createdOrg = organizationRepository.save(organization);
UriComponents uriComponents =
uriComponentsBuilder.path("/api/v0/organizations/{id}").buildAndExpand(createdOrg.getUuid());
URI location = uriComponents.toUri();
return ResponseEntity
.created(location)
.body(createdOrg);
}
/* update Organization */
......@@ -135,25 +155,38 @@ public class OrganizationController {
@ApiResponse(responseCode = "401", description = "unauthorised", content = @Content())
})
@PutMapping(path = "/{uuid}", consumes = MediaType.APPLICATION_JSON_VALUE)
public Organization updateOrganization(
public ResponseEntity<Organization> updateOrganization(
@Parameter(description="Organization to update or replace. This cannot be null or empty.",
schema=@Schema(implementation = Organization.class),
required=true) @Valid @RequestBody Organization newOrganization,
@Parameter(description = "Unique identifier of the organization that needs to be updated")
@PathVariable(name = "uuid") String uuid)
@PathVariable(name = "uuid") String uuid, UriComponentsBuilder uriComponentsBuilder)
{
return organizationRepository.findByUuid(uuid)
.map(organization -> {
organization.setAbbreviation(newOrganization.getAbbreviation());
organization.setName(newOrganization.getName());
organization.setImg(newOrganization.getImg());
organization.setUrl(newOrganization.getUrl());
return organizationRepository.save(organization);
})
.orElseGet(() -> {
newOrganization.setUuid(uuid);
return organizationRepository.save(newOrganization);
});
if (Boolean.TRUE.equals(CerebrumEntityUuidGenerator.isValid(uuid))) {
AtomicBoolean isCreated = new AtomicBoolean(false);
Organization org = organizationRepository.findByUuid(uuid)
.map(organization -> {
organization.setAbbreviation(newOrganization.getAbbreviation());
organization.setName(newOrganization.getName());
organization.setImg(newOrganization.getImg());
organization.setUrl(newOrganization.getUrl());
return organizationRepository.save(organization);
})
.orElseGet(() -> {
newOrganization.setUuid(uuid);
isCreated.set(true);
return organizationRepository.save(newOrganization);
});
if (isCreated.get()) {
UriComponents uriComponents =
uriComponentsBuilder.path("/api/v0/organizations/{id}").buildAndExpand(org.getUuid());
URI location = uriComponents.toUri();
return ResponseEntity.created(location).body(org);
}
return ResponseEntity.ok().body(org);
}
else throw new CerebrumInvalidUuidException(uuid);
}
/* JSON PATCH Organization */
......@@ -172,29 +205,33 @@ public class OrganizationController {
content = @Content(schema = @Schema(implementation = CerebrumApiError.class)))
})
@PatchMapping(path = "/{uuid}", consumes = "application/json-patch+json")
public Organization partialUpdateOrganization(
public ResponseEntity<Organization> partialUpdateOrganization(
@Parameter(description = "JSON Patch document structured as a JSON " +
"array of objects where each object contains one of the six " +
"JSON Patch operations: add, remove, replace, move, copy, and test",
schema = @Schema(implementation = JsonPatch.class),
required = true) @Valid @RequestBody JsonPatch patch,
@Parameter(description = "ID of the organization that needs to be partially updated")
@PathVariable(required = true) String uuid)
@PathVariable(name = "uuid") String uuid)
{
return organizationRepository.findByUuid(uuid)
.map(organization -> {
try {
Organization organizationPatched = applyPatchToOrganization(patch, organization);
return organizationRepository.save(organizationPatched);
} catch (JsonPatchException e) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "invalid id or json patch body", e);
} catch (JsonProcessingException e) {
throw new ResponseStatusException(
HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error", e);
}
})
.orElseThrow(()-> new CerebrumEntityNotFoundException("organization", uuid));
if (Boolean.TRUE.equals(CerebrumEntityUuidGenerator.isValid(uuid))) {
Organization partialUpdateOrganisation = organizationRepository.findByUuid(uuid)
.map(organization -> {
try {
Organization organizationPatched = applyPatchToOrganization(patch, organization);
return organizationRepository.save(organizationPatched);
} catch (JsonPatchException e) {
throw new ResponseStatusException(
HttpStatus.BAD_REQUEST, "json patch body", e);
} catch (JsonProcessingException e) {
throw new ResponseStatusException(
HttpStatus.INTERNAL_SERVER_ERROR, "Internal Server Error", e);
}
})
.orElseThrow(() -> new CerebrumEntityNotFoundException("organization", uuid));
return ResponseEntity.ok().body(partialUpdateOrganisation);
}
else throw new CerebrumInvalidUuidException(uuid);
}
/* delete Organization */
......
......@@ -6,6 +6,7 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
......@@ -15,7 +16,6 @@ import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
......@@ -30,10 +30,13 @@ import java.util.Objects;
import java.util.Set;
import de.helmholtz.marketplace.cerebrum.errorhandling.exception.CerebrumEntityNotFoundException;
import de.helmholtz.marketplace.cerebrum.errorhandling.exception.CerebrumInvalidUuidException;
@ControllerAdvice
public class CerebrumExceptionHandler extends ResponseEntityExceptionHandler
{
private static final String REGEX_VALUE = "^.|.$";
// modified version of: https://www.baeldung.com/global-error-handler-in-a-spring-rest-api
/**
* code: 400
......@@ -170,6 +173,36 @@ public class CerebrumExceptionHandler extends ResponseEntityExceptionHandler
cerebrumApiError, new HttpHeaders(), cerebrumApiError.getStatus());
}
// 400
@Override
protected ResponseEntity<Object> handleHttpMessageNotReadable(
final HttpMessageNotReadableException ex,
final HttpHeaders headers,
final HttpStatus status,
final WebRequest request)
{
logger.info(ex.getClass().getName());
final String error = "Malformed JSON or JSON+PATCH request";
final CerebrumApiError cerebrumApiError =
new CerebrumApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
return new ResponseEntity<>(
cerebrumApiError, new HttpHeaders(), cerebrumApiError.getStatus());
}
// 400
@ExceptionHandler({CerebrumInvalidUuidException.class})
private ResponseEntity<Object> handleInvalidUuid(
final CerebrumInvalidUuidException ex,
WebRequest request)
{
final String error = "Invalid uuid";
final CerebrumApiError cerebrumApiError =
new CerebrumApiError(HttpStatus.BAD_REQUEST, ex.getLocalizedMessage(), error);
return new ResponseEntity<>(
cerebrumApiError, new HttpHeaders(), cerebrumApiError.getStatus());
}
// 404
@ExceptionHandler({CerebrumEntityNotFoundException.class})
private ResponseEntity<Object> handleEntityNotFound(
......@@ -229,7 +262,7 @@ public class CerebrumExceptionHandler extends ResponseEntityExceptionHandler
} else if (size > 1) {
Set<HttpMethod> methods = ex.getSupportedHttpMethods();
builder.append("Supported media methods are ")
.append(methods.toString().replaceAll("^.|.$", ""));
.append(methods.toString().replaceAll(REGEX_VALUE, ""));
builder.replace(
builder.lastIndexOf(", "),
......@@ -270,7 +303,7 @@ public class CerebrumExceptionHandler extends ResponseEntityExceptionHandler
} else if (size > 1) {
List<MediaType> mediaTypes = ex.getSupportedMediaTypes();
builder.append("Acceptable MIME types are ")
.append(mediaTypes.toString().replaceAll("^.|.$", ""));
.append(mediaTypes.toString().replaceAll(REGEX_VALUE, ""));
builder.replace(
builder.lastIndexOf(", "),
......@@ -310,7 +343,7 @@ public class CerebrumExceptionHandler extends ResponseEntityExceptionHandler
} else if (size > 1) {
List<MediaType> mediaTypes = ex.getSupportedMediaTypes();
builder.append("Supported media types are ")
.append(mediaTypes.toString().replaceAll("^.|.$", ""));
.append(mediaTypes.toString().replaceAll(REGEX_VALUE, ""));
builder.replace(
builder.lastIndexOf(", "),
......
package de.helmholtz.marketplace.cerebrum.errorhandling.exception;
public class CerebrumInvalidUuidException extends RuntimeException
{
public CerebrumInvalidUuidException(String uuid)
{
super(uuid + " is an invalid uuid");
}
}
......@@ -11,5 +11,5 @@ public interface OrganizationRepository
{
Optional<Organization> findByUuid(String uuid);
void deleteByUuid(String id);
Long deleteByUuid(String id);
}
......@@ -67,10 +67,10 @@ public class CerebrumEntityUuidGenerator implements IdStrategy
public static Boolean isValid(String id)
{
try {
PrefixEnum.checkPrefixValidity(id.split("")[0]);
PrefixEnum.checkPrefixValidity(id.split("-")[0]);
UUID uuid = UUID.fromString(id.substring(id.indexOf('-') + 1));
return uuid.version() > 0;
} catch (IllegalArgumentException ex) {
} catch (IllegalArgumentException | NullPointerException ex) {
return false;
}
}
......
......@@ -6,6 +6,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
......@@ -13,10 +14,15 @@ import org.springframework.test.web.servlet.MvcResult;
import java.util.HashMap;
import java.util.Map;
import de.helmholtz.marketplace.cerebrum.entities.Organization;
import de.helmholtz.marketplace.cerebrum.repository.OrganizationRepository;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
......@@ -30,6 +36,7 @@ public class CerebrumExceptionHandlerTest
@Value("${cerebrum.test.oauth2-token}") private String TOKEN;
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
@MockBean private OrganizationRepository mockRepository;
@Test
public void whenTry_thenOK() throws Exception
......@@ -46,7 +53,6 @@ public class CerebrumExceptionHandlerTest
{
final MvcResult response = mockMvc.perform(get(ORG_API_URI + "?page=ccc "))
.andExpect(status().isBadRequest())
.andDo(print())
.andReturn();
final CerebrumApiError error = objectMapper.readValue(
response.getResponse().getContentAsString(), CerebrumApiError.class);
......@@ -55,15 +61,63 @@ public class CerebrumExceptionHandlerTest
assertTrue(error.getErrors().get(0).contains("should be of type java.lang.Integer"));
}
// handleHttpMessageNotReadable
@Test
public void whenHttpMessageNotReadable_thenBadRequest() throws Exception
{
Map<String, String> patch = new HashMap<>();
patch.put("path", "/abbreviation");
patch.put("value", "KI3T");
final MvcResult response = mockMvc.perform(
patch(ORG_API_URI + "/org-5189a7bc-d630-11ea-87d0-0242ac130003")
.header("Authorization", "Bearer " + TOKEN)
.accept("application/json")
.contentType("application/json-patch+json").content(objectMapper.writeValueAsString(patch)))
.andExpect(status().isBadRequest())
.andReturn();
final CerebrumApiError error = objectMapper.readValue(
response.getResponse().getContentAsString(), CerebrumApiError.class);
assertEquals(HttpStatus.BAD_REQUEST, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getErrors().get(0).contains("Malformed JSON or JSON+PATCH request"));
}
// handleInvalidUuid
@Test
public void whenInvalidUuid_thenBadRequest() throws Exception
{
String badUuid = "abc";
final MvcResult response = mockMvc.perform(
get(ORG_API_URI + "/" + badUuid))
.andExpect(status().isBadRequest())
.andReturn();
final CerebrumApiError error = objectMapper.readValue(
response.getResponse().getContentAsString(), CerebrumApiError.class);
assertEquals(HttpStatus.BAD_REQUEST, error.getStatus());
assertEquals(1, error.getErrors().size());
assertTrue(error.getMessage()
.contains(badUuid + " is an invalid uuid"));
}
// handleEntityNotFound
@Test
public void whenEntityNotFound_thenNotFound() throws Exception
{
String nonExistingUuid = "org-5189a7bc-d630-11ea-87d0-0242ac130003";
String nonExistingUuid = "org-5189a7bc-d630-11ea-87d0-0242ac130004";
Organization kit = new Organization();
kit.setName("Karlsruher Institut fuer Technologie");
kit.setAbbreviation("KIT");
kit.setUrl("http://www.kit.edu/");
kit.setImg("http://www.kit.edu/img/intern/kit_logo_V2_de.svg");
kit.setUuid("org-5189a7bc-d630-11ea-87d0-0242ac130003");
given(mockRepository.findByUuid("org-5189a7bc-d630-11ea-87d0-0242ac130003"))
.willReturn(java.util.Optional.of(kit));
final MvcResult response = mockMvc.perform(
get(ORG_API_URI + "/" + nonExistingUuid))
.andExpect(status().isNotFound())
.andDo(print())
.andReturn();
final CerebrumApiError error = objectMapper.readValue(
response.getResponse().getContentAsString(), CerebrumApiError.class);
......@@ -85,7 +139,6 @@ public class CerebrumExceptionHandlerTest
.contentType("application/json")
.header("Authorization", "Bearer " + TOKEN)
.content(objectMapper.writeValueAsString(organisation)))
.andDo(print())
.andExpect(status().isBadRequest())
.andReturn();
final CerebrumApiError error = objectMapper.readValue(
......@@ -102,7 +155,6 @@ public class CerebrumExceptionHandlerTest
final MvcResult response = mockMvc.perform(delete(ORG_API_URI)
.header("Authorization", "Bearer " + TOKEN))
.andExpect(status().isMethodNotAllowed())
.andDo(print())
.andReturn();
final CerebrumApiError error = objectMapper.readValue(
response.getResponse().getContentAsString(), CerebrumApiError.class);
......@@ -119,7 +171,6 @@ public class CerebrumExceptionHandlerTest
final MvcResult response = mockMvc.perform(delete(API_URI_PREFIX + "/xx")
.header("Authorization", "Bearer " + TOKEN))
.andExpect(status().isNotFound())
.andDo(print())
.andReturn();
final CerebrumApiError error = objectMapper.readValue(
response.getResponse().getContentAsString(), CerebrumApiError.class);
......@@ -142,7 +193,6 @@ public class CerebrumExceptionHandlerTest
.header("Authorization", "Bearer " + TOKEN)
.content(objectMapper.writeValueAsString(organisation)))
.andExpect(status().isUnsupportedMediaType())
.andDo(print())
.andReturn();
final CerebrumApiError error = objectMapper.readValue(
response.getResponse().getContentAsString(), CerebrumApiError.class);
......@@ -157,7 +207,6 @@ public class CerebrumExceptionHandlerTest
{
final MvcResult response = mockMvc.perform(get(ORG_API_URI)
.accept("application/xml"))
.andDo(print())
.andExpect(status().isNotAcceptable())
.andReturn();
assertEquals(406, response.getResponse().getStatus());
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment