From c82aebcde26e34c4151531b4d7a8f6e7689734ba Mon Sep 17 00:00:00 2001 From: "andre.schmid" Date: Fri, 14 May 2021 20:38:45 +0100 Subject: Add models imports endpoint and persistence structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create the structure to persist the model imports. Changed create model API, allowing to create a model along its TOSCA descriptor import structure. Introduced an endpoint to update the imports of a model. Change-Id: Ic775ef544051c29c721cacc20b37c2fb20338be9 Issue-ID: SDC-3614 Signed-off-by: André Schmid --- catalog-be/pom.xml | 3 +- .../files/default/error-configuration.yaml | 22 ++++ .../sdc/be/components/impl/ModelBusinessLogic.java | 63 +++++++++- .../openecomp/sdc/be/servlets/ModelServlet.java | 65 +++++++--- .../exception/OperationExceptionMapper.java | 11 +- .../be/components/impl/ModelBusinessLogicTest.java | 137 ++++++++++++++++++-- .../beans/ToscaModelImportCassandraDaoMock.java | 39 ++++++ .../sdc/be/servlets/ModelServletTest.java | 138 ++++++++++++++++++--- .../resources/modelImports/emptyModelImports.zip | Bin 0 -> 186 bytes .../modelWithSubFolderAndEmptyFolder.zip | Bin 0 -> 1961 bytes .../src/test/resources/paths/path-context.xml | 1 + 11 files changed, 426 insertions(+), 53 deletions(-) create mode 100644 catalog-be/src/test/java/org/openecomp/sdc/be/components/path/beans/ToscaModelImportCassandraDaoMock.java create mode 100644 catalog-be/src/test/resources/modelImports/emptyModelImports.zip create mode 100644 catalog-be/src/test/resources/modelImports/modelWithSubFolderAndEmptyFolder.zip (limited to 'catalog-be') diff --git a/catalog-be/pom.xml b/catalog-be/pom.xml index 3f56c02afa..53af2c7b27 100644 --- a/catalog-be/pom.xml +++ b/catalog-be/pom.xml @@ -1221,8 +1221,7 @@ - ${project.parent.basedir}/catalog-be/target - + ${project.build.directory} normatives.tar.gz diff --git a/catalog-be/src/main/docker/backend/chef-repo/cookbooks/sdc-catalog-be/files/default/error-configuration.yaml b/catalog-be/src/main/docker/backend/chef-repo/cookbooks/sdc-catalog-be/files/default/error-configuration.yaml index 2c6a0c852b..b277aeef2f 100644 --- a/catalog-be/src/main/docker/backend/chef-repo/cookbooks/sdc-catalog-be/files/default/error-configuration.yaml +++ b/catalog-be/src/main/docker/backend/chef-repo/cookbooks/sdc-catalog-be/files/default/error-configuration.yaml @@ -2473,3 +2473,25 @@ errors: message: "Error: Model name '%1' already exists.", messageId: "SVC4144" } + + #---------SVC4145------------------------------ + # %1 - "Model name" + INVALID_MODEL: { + code: 400, + message: "Invalid model '%1'.", + messageId: "SVC4145" + } + + #---------SVC4146------------------------------ + MODEL_IMPORTS_IS_EMPTY: { + code: 400, + message: "Given model imports zip is empty.", + messageId: "SVC4146" + } + + #---------SVC4147------------------------------ + COULD_NOT_READ_MODEL_IMPORTS: { + code: 400, + message: "Could not read imports zip.", + messageId: "SVC4147" + } diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ModelBusinessLogic.java b/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ModelBusinessLogic.java index 1ef4cef701..7f68a00a8b 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ModelBusinessLogic.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/components/impl/ModelBusinessLogic.java @@ -18,17 +18,25 @@ */ package org.openecomp.sdc.be.components.impl; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.Optional; +import org.apache.commons.lang3.StringUtils; import org.openecomp.sdc.be.model.Model; +import org.openecomp.sdc.be.model.jsonjanusgraph.operations.exception.ModelOperationExceptionSupplier; import org.openecomp.sdc.be.model.operations.impl.ModelOperation; +import org.openecomp.sdc.common.zip.ZipUtils; +import org.openecomp.sdc.common.zip.exception.ZipException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; - @Component("modelBusinessLogic") public class ModelBusinessLogic { - private static final Logger log = LoggerFactory.getLogger(ModelBusinessLogic.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ModelBusinessLogic.class); private final ModelOperation modelOperation; @Autowired @@ -37,7 +45,56 @@ public class ModelBusinessLogic { } public Model createModel(final Model model) { - log.debug("createModel: creating model {}", model); + LOGGER.debug("createModel: creating model {}", model); return modelOperation.createModel(model, false); } + + public Optional findModel(final String modelName) { + if (StringUtils.isEmpty(modelName)) { + return Optional.empty(); + } + return modelOperation.findModelByName(modelName); + } + + public void createModelImports(final String modelName, final InputStream modelImportsZip) { + if (StringUtils.isEmpty(modelName)) { + throw ModelOperationExceptionSupplier.invalidModel(modelName).get(); + } + if (modelImportsZip == null) { + throw ModelOperationExceptionSupplier.emptyModelImports().get(); + } + if (findModel(modelName).isEmpty()) { + throw ModelOperationExceptionSupplier.invalidModel(modelName).get(); + } + + final var fileBytes = readBytes(modelImportsZip); + final Map zipFilesPathContentMap = unzipInMemory(fileBytes); + if (zipFilesPathContentMap.isEmpty()) { + throw ModelOperationExceptionSupplier.emptyModelImports().get(); + } + + modelOperation.createModelImports(modelName, zipFilesPathContentMap); + } + + private Map unzipInMemory(final byte[] fileBytes) { + try { + return ZipUtils.readZip(fileBytes, false); + } catch (final ZipException e) { + throw ModelOperationExceptionSupplier.couldNotReadImports().get(); + } + } + + private byte[] readBytes(final InputStream modelImportsZip) { + try (final InputStream in = modelImportsZip; final ByteArrayOutputStream os = new ByteArrayOutputStream()) { + final var buffer = new byte[1024]; + int len; + while ((len = in.read(buffer)) != -1) { + os.write(buffer, 0, len); + } + return os.toByteArray(); + } catch (final IOException e) { + LOGGER.debug("Could not read the model imports zip", e); + throw ModelOperationExceptionSupplier.couldNotReadImports().get(); + } + } } \ No newline at end of file diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ModelServlet.java b/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ModelServlet.java index f4fc883b4c..0c5e4aebd6 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ModelServlet.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/ModelServlet.java @@ -28,6 +28,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.servers.Server; import io.swagger.v3.oas.annotations.tags.Tag; +import java.io.InputStream; import java.util.Arrays; import javax.inject.Inject; import javax.validation.Valid; @@ -35,10 +36,13 @@ import javax.validation.constraints.NotNull; import javax.ws.rs.Consumes; import javax.ws.rs.HeaderParam; import javax.ws.rs.POST; +import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; +import org.glassfish.jersey.media.multipart.FormDataParam; import org.openecomp.sdc.be.components.impl.ComponentInstanceBusinessLogic; import org.openecomp.sdc.be.components.impl.ModelBusinessLogic; import org.openecomp.sdc.be.components.impl.ResourceImportManager; @@ -55,10 +59,10 @@ import org.openecomp.sdc.be.ui.model.ModelCreateRequest; import org.openecomp.sdc.be.user.Role; import org.openecomp.sdc.be.user.UserBusinessLogic; import org.openecomp.sdc.common.api.Constants; +import org.openecomp.sdc.common.util.ValidationUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestBody; /** * Root resource (exposed at "/" path) @@ -85,34 +89,67 @@ public class ModelServlet extends AbstractValidationsServlet { @POST @Path("/model") - @Consumes(MediaType.APPLICATION_JSON) + @Consumes(MediaType.MULTIPART_FORM_DATA) @Produces(MediaType.APPLICATION_JSON) - @Operation(description = "Create model", method = "POST", summary = "Returns created model", responses = { + @PermissionAllowed(AafPermission.PermNames.INTERNAL_ALL_VALUE) + @Operation(description = "Create a TOSCA model, along with its imports files", method = "POST", summary = "Create a TOSCA model", responses = { @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = Response.class)))), @ApiResponse(responseCode = "201", description = "Model created"), - @ApiResponse(responseCode = "403", description = "Restricted operation"), @ApiResponse(responseCode = "400", description = "Invalid content / Missing content"), - @ApiResponse(responseCode = "409", description = "Resource already exists")}) - @PermissionAllowed(AafPermission.PermNames.INTERNAL_ALL_VALUE) + @ApiResponse(responseCode = "403", description = "Restricted operation"), + @ApiResponse(responseCode = "409", description = "Model already exists")}) public Response createModel(@Parameter(description = "model to be created", required = true) - @Valid @RequestBody @NotNull final ModelCreateRequest modelCreateRequest, - @HeaderParam(value = Constants.USER_ID_HEADER) String userId) { - - validateUser(userId); + @NotNull @Valid @FormDataParam("model") final ModelCreateRequest modelCreateRequest, + @Parameter(description = "the model TOSCA imports zipped", required = true) + @NotNull @FormDataParam("modelImportsZip") final InputStream modelImportsZip, + @HeaderParam(value = Constants.USER_ID_HEADER) final String userId) { + validateUser(ValidationUtils.sanitizeInputString(userId)); + final var modelName = ValidationUtils.sanitizeInputString(modelCreateRequest.getName().trim()); try { - final Model modelCreateResponse = modelBusinessLogic + final Model createdModel = modelBusinessLogic .createModel(new JMapper<>(Model.class, ModelCreateRequest.class).getDestination(modelCreateRequest)); + modelBusinessLogic.createModelImports(modelName, modelImportsZip); return buildOkResponse(getComponentsUtils().getResponseFormat(ActionStatus.CREATED), - RepresentationUtils.toRepresentation(modelCreateResponse)); + RepresentationUtils.toRepresentation(createdModel)); + } catch (final BusinessException e) { + throw e; + } catch (final Exception e) { + var errorMsg = String.format("Unexpected error while creating model '%s' imports", modelName); + BeEcompErrorManager.getInstance().logBeRestApiGeneralError(errorMsg); + log.error(errorMsg, e); + return buildErrorResponse(getComponentsUtils().getResponseFormat(ActionStatus.GENERAL_ERROR)); + } + } + + @PUT + @Path("/model/imports") + @Consumes(MediaType.MULTIPART_FORM_DATA) + @Produces(MediaType.APPLICATION_JSON) + @PermissionAllowed(AafPermission.PermNames.INTERNAL_ALL_VALUE) + @Operation(description = "Update a model TOSCA imports", method = "PUT", summary = "Update a model TOSCA imports", responses = { + @ApiResponse(content = @Content(array = @ArraySchema(schema = @Schema(implementation = Response.class)))), + @ApiResponse(responseCode = "204", description = "Model imports updated"), + @ApiResponse(responseCode = "400", description = "Invalid content / Missing content"), + @ApiResponse(responseCode = "403", description = "Restricted operation"), + @ApiResponse(responseCode = "404", description = "Model not found")}) + public Response updateModelImports(@Parameter(description = "model to be created", required = true) + @NotNull @FormDataParam("modelName") String modelName, + @Parameter(description = "the model TOSCA imports zipped", required = true) + @NotNull @FormDataParam("modelImportsZip") final InputStream modelImportsZip, + @HeaderParam(value = Constants.USER_ID_HEADER) final String userId) { + validateUser(ValidationUtils.sanitizeInputString(userId)); + modelName = ValidationUtils.sanitizeInputString(modelName); + try { + modelBusinessLogic.createModelImports(modelName, modelImportsZip); } catch (final BusinessException e) { throw e; } catch (final Exception e) { - var errorMsg = String - .format("Unexpected error while creating model '%s'", modelCreateRequest.getName()); + var errorMsg = String.format("Unexpected error while creating model '%s' imports", modelName); BeEcompErrorManager.getInstance().logBeRestApiGeneralError(errorMsg); log.error(errorMsg, e); return buildErrorResponse(getComponentsUtils().getResponseFormat(ActionStatus.GENERAL_ERROR)); } + return Response.status(Status.NO_CONTENT).build(); } private void validateUser(final String userId) { diff --git a/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/exception/OperationExceptionMapper.java b/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/exception/OperationExceptionMapper.java index 7c25f8aef8..062e03b0da 100644 --- a/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/exception/OperationExceptionMapper.java +++ b/catalog-be/src/main/java/org/openecomp/sdc/be/servlets/exception/OperationExceptionMapper.java @@ -26,6 +26,7 @@ import org.openecomp.sdc.be.components.impl.ResponseFormatManager; import org.openecomp.sdc.be.model.jsonjanusgraph.operations.exception.OperationException; import org.openecomp.sdc.be.servlets.builder.ServletResponseBuilder; import org.openecomp.sdc.common.log.wrappers.Logger; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component @@ -35,16 +36,22 @@ public class OperationExceptionMapper implements ExceptionMapper modelBusinessLogic.createModel(model)); assertThat(((OperationException) exception).getActionStatus().name()).isEqualTo(ActionStatus.MODEL_ALREADY_EXISTS.name()); assertThat(((OperationException) exception).getParams()).contains(model.getName()); } + @Test + void createModelImportsSuccessTest() throws IOException, ZipException { + final var modelId = "modelId"; + final var resolve = modelImportsResourcePath.resolve("modelWithSubFolderAndEmptyFolder.zip"); + final var zipBytes = Files.readAllBytes(resolve); + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(zipBytes); + final Map expectedZipMap = ZipUtils.readZip(zipBytes, false); + + when(modelOperation.findModelByName(modelId)).thenReturn(Optional.of(new Model(modelId))); + doNothing().when(modelOperation).createModelImports(eq(modelId), anyMap()); + + modelBusinessLogic.createModelImports(modelId, byteArrayInputStream); + + final ArgumentCaptor> zipMapArgumentCaptor = ArgumentCaptor.forClass(Map.class); + verify(modelOperation).createModelImports(eq(modelId), zipMapArgumentCaptor.capture()); + expectedZipMap.keySet().forEach(key -> assertTrue(zipMapArgumentCaptor.getValue().containsKey(key), "Expecting import " + key)); + } + + @Test + void createModelImportsTest_invalidModel() { + //given an empty model + final var modelId = ""; + + final var emptyByteArrayInputStream = new ByteArrayInputStream(new byte[0]); + var actualOperationException = assertThrows(OperationException.class, + () -> modelBusinessLogic.createModelImports(modelId, emptyByteArrayInputStream)); + + var expectedOperationException = ModelOperationExceptionSupplier.invalidModel(modelId).get(); + assertEquals(actualOperationException.getActionStatus(), expectedOperationException.getActionStatus()); + assertEquals(actualOperationException.getParams().length, expectedOperationException.getParams().length); + assertEquals(actualOperationException.getParams()[0], expectedOperationException.getParams()[0]); + + //given a null model + actualOperationException = assertThrows(OperationException.class, + () -> modelBusinessLogic.createModelImports(null, emptyByteArrayInputStream)); + + expectedOperationException = ModelOperationExceptionSupplier.invalidModel(null).get(); + assertEquals(actualOperationException.getActionStatus(), expectedOperationException.getActionStatus()); + assertEquals(actualOperationException.getParams().length, expectedOperationException.getParams().length); + assertEquals(actualOperationException.getParams()[0], expectedOperationException.getParams()[0]); + } + + @Test + void createModelImportsTest_nullInputStream() { + final var modelId = "modelId"; + + final OperationException actualOperationException = assertThrows(OperationException.class, + () -> modelBusinessLogic.createModelImports(modelId, null)); + + final OperationException expectedOperationException = ModelOperationExceptionSupplier.emptyModelImports().get(); + assertEquals(actualOperationException.getActionStatus(), expectedOperationException.getActionStatus()); + assertEquals(actualOperationException.getParams().length, expectedOperationException.getParams().length); + } + + @Test + void createModelImportsTest_emptyModelImports() throws IOException { + final var modelId = "modelId"; + + final var resolve = modelImportsResourcePath.resolve("emptyModelImports.zip"); + final var zipBytes = Files.readAllBytes(resolve); + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(zipBytes); + + when(modelOperation.findModelByName(modelId)).thenReturn(Optional.of(new Model(modelId))); + + final OperationException actualOperationException = assertThrows(OperationException.class, + () -> modelBusinessLogic.createModelImports(modelId, byteArrayInputStream)); + + final OperationException expectedOperationException = ModelOperationExceptionSupplier.emptyModelImports().get(); + assertEquals(actualOperationException.getActionStatus(), expectedOperationException.getActionStatus()); + assertEquals(actualOperationException.getParams().length, expectedOperationException.getParams().length); + } + + @Test + void createModelImportsTest_modelNotFound() { + final var modelId = "modelId"; + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(new byte[0]); + + when(modelOperation.findModelByName(modelId)).thenReturn(Optional.empty()); + + final OperationException actualOperationException = assertThrows(OperationException.class, + () -> modelBusinessLogic.createModelImports(modelId, byteArrayInputStream)); + + final OperationException expectedOperationException = ModelOperationExceptionSupplier.invalidModel(modelId).get(); + assertEquals(actualOperationException.getActionStatus(), expectedOperationException.getActionStatus()); + assertEquals(actualOperationException.getParams().length, expectedOperationException.getParams().length); + } + + @Test + void findModelSuccessTest() { + final var modelId = "modelId"; + when(modelOperation.findModelByName(modelId)).thenReturn(Optional.of(new Model(modelId))); + final Optional actualModel = modelBusinessLogic.findModel(modelId); + assertTrue(actualModel.isPresent()); + assertEquals(new Model(modelId), actualModel.get()); + } + + @Test + void findModelTest_emptyOrNullModelName() { + when(modelOperation.findModelByName(anyString())).thenReturn(Optional.of(new Model())); + var actualModel = modelBusinessLogic.findModel(""); + assertTrue(actualModel.isEmpty()); + actualModel = modelBusinessLogic.findModel(null); + assertTrue(actualModel.isEmpty()); + } } \ No newline at end of file diff --git a/catalog-be/src/test/java/org/openecomp/sdc/be/components/path/beans/ToscaModelImportCassandraDaoMock.java b/catalog-be/src/test/java/org/openecomp/sdc/be/components/path/beans/ToscaModelImportCassandraDaoMock.java new file mode 100644 index 0000000000..9f293b1a1c --- /dev/null +++ b/catalog-be/src/test/java/org/openecomp/sdc/be/components/path/beans/ToscaModelImportCassandraDaoMock.java @@ -0,0 +1,39 @@ +/* + * ============LICENSE_START======================================================= + * Copyright (C) 2021 Nordix Foundation + * ================================================================================ + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * ============LICENSE_END========================================================= + */ + +package org.openecomp.sdc.be.components.path.beans; + +import javax.annotation.PostConstruct; +import org.openecomp.sdc.be.dao.cassandra.CassandraClient; +import org.openecomp.sdc.be.dao.cassandra.ToscaModelImportCassandraDao; +import org.springframework.stereotype.Component; + +@Component("tosca-model-import-cassandra-dao") +public class ToscaModelImportCassandraDaoMock extends ToscaModelImportCassandraDao { + + public ToscaModelImportCassandraDaoMock(final CassandraClient cassandraClient) { + super(cassandraClient); + } + + @PostConstruct + @Override + public void init() { + + } +} diff --git a/catalog-be/src/test/java/org/openecomp/sdc/be/servlets/ModelServletTest.java b/catalog-be/src/test/java/org/openecomp/sdc/be/servlets/ModelServletTest.java index 34201d3c1f..5992be4e9d 100644 --- a/catalog-be/src/test/java/org/openecomp/sdc/be/servlets/ModelServletTest.java +++ b/catalog-be/src/test/java/org/openecomp/sdc/be/servlets/ModelServletTest.java @@ -18,19 +18,28 @@ */ package org.openecomp.sdc.be.servlets; -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.InputStream; +import java.nio.file.Path; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; -import javax.validation.ConstraintViolationException; -import javax.ws.rs.core.Response; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response.Status; import org.apache.commons.lang3.StringUtils; import org.eclipse.jetty.http.HttpStatus; import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.client.ClientConfig; +import org.glassfish.jersey.media.multipart.FormDataMultiPart; +import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.test.JerseyTest; import org.glassfish.jersey.test.TestProperties; @@ -46,6 +55,7 @@ import org.mockito.MockitoAnnotations; import org.openecomp.sdc.be.components.impl.ComponentInstanceBusinessLogic; import org.openecomp.sdc.be.components.impl.ModelBusinessLogic; import org.openecomp.sdc.be.components.impl.ResourceImportManager; +import org.openecomp.sdc.be.components.impl.ResponseFormatManager; import org.openecomp.sdc.be.components.validation.UserValidations; import org.openecomp.sdc.be.config.ConfigurationManager; import org.openecomp.sdc.be.config.SpringConfig; @@ -55,6 +65,9 @@ import org.openecomp.sdc.be.impl.ComponentsUtils; import org.openecomp.sdc.be.impl.ServletUtils; import org.openecomp.sdc.be.impl.WebAppContextWrapper; import org.openecomp.sdc.be.model.Model; +import org.openecomp.sdc.be.model.jsonjanusgraph.operations.exception.OperationException; +import org.openecomp.sdc.be.servlets.builder.ServletResponseBuilder; +import org.openecomp.sdc.be.servlets.exception.OperationExceptionMapper; import org.openecomp.sdc.be.ui.model.ModelCreateRequest; import org.openecomp.sdc.be.user.UserBusinessLogic; import org.openecomp.sdc.common.api.ConfigurationSource; @@ -100,13 +113,16 @@ class ModelServletTest extends JerseyTest { @Mock private UserValidations userValidations; + @Mock + private ResponseFormatManager responseFormatManager; + private Model model; - private Response response; private ModelCreateRequest modelCreateRequest; + private final Path rootPath = Path.of("/v1/catalog/model"); + private final Path importsPath = rootPath.resolve("imports"); @BeforeAll public void initClass() { - MockitoAnnotations.openMocks(this); when(request.getSession()).thenReturn(session); when(session.getServletContext()).thenReturn(servletContext); when(servletContext.getAttribute(Constants.WEB_APPLICATION_CONTEXT_WRAPPER_ATTR)) @@ -145,6 +161,7 @@ class ModelServletTest extends JerseyTest { @Override protected ResourceConfig configure() { + MockitoAnnotations.openMocks(this); forceSet(TestProperties.CONTAINER_PORT, "0"); final ApplicationContext context = new AnnotationConfigApplicationContext(SpringConfig.class); return new ResourceConfig(ModelServlet.class) @@ -158,51 +175,132 @@ class ModelServletTest extends JerseyTest { bind(servletUtils).to(ServletUtils.class); bind(resourceImportManager).to(ResourceImportManager.class); bind(modelBusinessLogic).to(ModelBusinessLogic.class); + bind(userValidations).to(UserValidations.class); } }) + .register(new OperationExceptionMapper(new ServletResponseBuilder(), responseFormatManager)) + .register(MultiPartFeature.class) .property("contextConfig", context); } + @Override + protected void configureClient(final ClientConfig config) { + config.register(MultiPartFeature.class); + } + @Test - void createModelSuccessTest() { + void createModelSuccessTest() throws JsonProcessingException { when(responseFormat.getStatus()).thenReturn(HttpStatus.OK_200); when(componentsUtils.getResponseFormat(ActionStatus.CREATED)).thenReturn(responseFormat); when(modelBusinessLogic.createModel(any(Model.class))).thenReturn(model); - response = modelServlet.createModel(modelCreateRequest, USER_ID); - assertThat(response.getStatus()).isEqualTo(HttpStatus.OK_200); + final FormDataMultiPart formDataMultiPart = buildCreateFormDataMultiPart(new byte[0], parseToJsonString(modelCreateRequest)); + final var response = target(rootPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .post(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.OK.getStatusCode(), response.getStatus()); } @Test - void createModelFailTest() { + void createModelFailTest() throws JsonProcessingException { when(responseFormat.getStatus()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR_500); when(componentsUtils.getResponseFormat(ActionStatus.GENERAL_ERROR)).thenReturn(responseFormat); when(modelBusinessLogic.createModel(any(Model.class))).thenReturn(model); - response = modelServlet.createModel(modelCreateRequest, USER_ID); - assertThat(response.getStatus()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR_500); + final FormDataMultiPart formDataMultiPart = buildCreateFormDataMultiPart(new byte[0], parseToJsonString(modelCreateRequest)); + final var response = target(rootPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .post(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); } @Test - void createModelFailWithModelNameEmptyTest() { + void createModelFailWithModelNameEmptyTest() throws JsonProcessingException { when(responseFormat.getStatus()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR_500); when(componentsUtils.getResponseFormat(ActionStatus.GENERAL_ERROR)).thenReturn(responseFormat); modelCreateRequest.setName(StringUtils.EMPTY); - final Exception exception = assertThrows(ConstraintViolationException.class, () -> modelServlet.createModel(modelCreateRequest, USER_ID)); - assertThat(exception.getMessage()).isEqualTo("Model name cannot be empty"); + final FormDataMultiPart formDataMultiPart = buildCreateFormDataMultiPart(new byte[0], parseToJsonString(modelCreateRequest)); + final var response = target(rootPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .post(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); } @Test - void createModelFailWithModelNameNullTest() { + void createModelFailWithModelNameNullTest() throws JsonProcessingException { when(responseFormat.getStatus()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR_500); when(componentsUtils.getResponseFormat(ActionStatus.GENERAL_ERROR)).thenReturn(responseFormat); modelCreateRequest.setName(null); - final Exception exception = assertThrows(ConstraintViolationException.class, () -> modelServlet.createModel(modelCreateRequest, USER_ID)); - assertThat(exception.getMessage()).isEqualTo("Model name cannot be null"); + final var modelFile = new byte[0]; + final FormDataMultiPart formDataMultiPart = buildCreateFormDataMultiPart(modelFile, parseToJsonString(modelCreateRequest)); + final var response = target(rootPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .post(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); } @Test - void createModelThrowsBusinessExceptionTest() { + void createModelThrowsBusinessExceptionTest() throws JsonProcessingException { + final var modelFile = new byte[0]; + final String modelCreateAsJson = parseToJsonString(modelCreateRequest); + final FormDataMultiPart formDataMultiPart = buildCreateFormDataMultiPart(modelFile, modelCreateAsJson); when(modelBusinessLogic.createModel(model)).thenThrow(new BusinessException() {}); - assertThrows(BusinessException.class, () -> modelServlet.createModel(modelCreateRequest, USER_ID)); + + final var response = target(rootPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .post(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + } + + @Test + void updateModelImportsSuccessTest() { + final FormDataMultiPart formDataMultiPart = buildUpdateFormDataMultiPart("model1", new byte[0]); + + final var response = target(importsPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .put(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus()); + } + + @Test + void updateModelImports_businessException() { + final var modelId = "model1"; + final FormDataMultiPart formDataMultiPart = buildUpdateFormDataMultiPart(modelId, new byte[0]); + final OperationException operationException = new OperationException(ActionStatus.INVALID_MODEL, modelId); + doThrow(operationException).when(modelBusinessLogic).createModelImports(eq(modelId), any(InputStream.class)); + when(responseFormatManager.getResponseFormat(ActionStatus.INVALID_MODEL, modelId)) + .thenReturn(new ResponseFormat(Status.BAD_REQUEST.getStatusCode())); + final var response = target(importsPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .put(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus()); + } + + @Test + void updateModelImports_unknownException() { + final var modelName = "model1"; + final FormDataMultiPart formDataMultiPart = buildUpdateFormDataMultiPart(modelName, new byte[0]); + doThrow(new RuntimeException()).when(modelBusinessLogic).createModelImports(eq(modelName), any(InputStream.class)); + when(responseFormat.getStatus()).thenReturn(HttpStatus.INTERNAL_SERVER_ERROR_500); + when(componentsUtils.getResponseFormat(ActionStatus.GENERAL_ERROR)).thenReturn(responseFormat); + final var response = target(importsPath.toString()).request(MediaType.APPLICATION_JSON) + .header(Constants.USER_ID_HEADER, USER_ID) + .put(Entity.entity(formDataMultiPart, MediaType.MULTIPART_FORM_DATA)); + assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), response.getStatus()); + } + + private FormDataMultiPart buildUpdateFormDataMultiPart(final String modelName, final byte[] importFilesZip) { + return new FormDataMultiPart() + .field("modelName", modelName) + .field("modelImportsZip", importFilesZip, MediaType.MULTIPART_FORM_DATA_TYPE); + } + + private FormDataMultiPart buildCreateFormDataMultiPart(final byte[] modelFile, final String modelCreateAsJson) { + return new FormDataMultiPart() + .field("model", modelCreateAsJson, MediaType.APPLICATION_JSON_TYPE) + .field("modelImportsZip", modelFile, MediaType.MULTIPART_FORM_DATA_TYPE); + } + + private String parseToJsonString(final Object object) throws JsonProcessingException { + return new ObjectMapper().writeValueAsString(object); } } \ No newline at end of file diff --git a/catalog-be/src/test/resources/modelImports/emptyModelImports.zip b/catalog-be/src/test/resources/modelImports/emptyModelImports.zip new file mode 100644 index 0000000000..323472535d Binary files /dev/null and b/catalog-be/src/test/resources/modelImports/emptyModelImports.zip differ diff --git a/catalog-be/src/test/resources/modelImports/modelWithSubFolderAndEmptyFolder.zip b/catalog-be/src/test/resources/modelImports/modelWithSubFolderAndEmptyFolder.zip new file mode 100644 index 0000000000..281e3fc3c1 Binary files /dev/null and b/catalog-be/src/test/resources/modelImports/modelWithSubFolderAndEmptyFolder.zip differ diff --git a/catalog-be/src/test/resources/paths/path-context.xml b/catalog-be/src/test/resources/paths/path-context.xml index 3b472dd16f..7994b0c44d 100644 --- a/catalog-be/src/test/resources/paths/path-context.xml +++ b/catalog-be/src/test/resources/paths/path-context.xml @@ -80,6 +80,7 @@ Modifications copyright (c) 2018 Nokia + -- cgit 1.2.3-korg