From b14f04b6bd92b4dd6e3ed511ef5334db02e0b1ea Mon Sep 17 00:00:00 2001 From: Renu Kumari Date: Mon, 28 Mar 2022 10:22:41 -0400 Subject: Update CM-Handle registration response - update openapi.yml with new response structure - send only details of failed cm-handle operations - updated csit to validate 200 status Issue-ID: CPS-896 Signed-off-by: Renu Kumari Change-Id: I3b868bcc5b8ff488c31faef51edc82c771452234 --- cps-ncmp-rest/docs/openapi/components.yaml | 48 ++++++++++++ cps-ncmp-rest/docs/openapi/ncmp-inventory.yml | 7 +- .../NetworkCmProxyInventoryController.java | 60 +++++++++++++-- .../NetworkCmProxyInventoryControllerSpec.groovy | 87 ++++++++++++++++++++-- .../api/models/DmiPluginRegistrationResponse.java | 7 +- csit/tests/cps-model-sync/cps-model-sync.robot | 4 +- 6 files changed, 196 insertions(+), 17 deletions(-) diff --git a/cps-ncmp-rest/docs/openapi/components.yaml b/cps-ncmp-rest/docs/openapi/components.yaml index 69225aed2..ddce05242 100644 --- a/cps-ncmp-rest/docs/openapi/components.yaml +++ b/cps-ncmp-rest/docs/openapi/components.yaml @@ -70,6 +70,54 @@ components: items: type: string example: [my-cm-handle1, my-cm-handle2, my-cm-handle3] + DmiPluginRegistrationErrorResponse: + type: object + properties: + failedCreatedCmHandles: + type: array + items: + $ref: '#/components/schemas/CmHandlerRegistrationErrorResponse' + example: [ + { + "cmHandle": "my-cm-handle-01", + "errorCode": "01", + "errorText": "cm-handle already exists" + } + ] + failedUpdatedCmHandles: + type: array + items: + $ref: '#/components/schemas/CmHandlerRegistrationErrorResponse' + example: [ + { + "cmHandle": "my-cm-handle-02", + "errorCode": "02", + "errorText": "cm-handle does not exist" + } + ] + failedRemovedCmHandles: + type: array + items: + $ref: '#/components/schemas/CmHandlerRegistrationErrorResponse' + example: [ + { + "cmHandle": "my-cm-handle-02", + "errorCode": "02", + "errorText": "cm-handle does not exist" + } + ] + CmHandlerRegistrationErrorResponse: + type: object + properties: + cmHandle: + type: string + example: my-cm-handle + errorCode: + type: string + example: '01' + errorText: + type: string + example: 'cm-handle already exists' RestInputCmHandle: required: diff --git a/cps-ncmp-rest/docs/openapi/ncmp-inventory.yml b/cps-ncmp-rest/docs/openapi/ncmp-inventory.yml index 3cd8e8baf..5e61d0962 100755 --- a/cps-ncmp-rest/docs/openapi/ncmp-inventory.yml +++ b/cps-ncmp-rest/docs/openapi/ncmp-inventory.yml @@ -31,7 +31,7 @@ updateDmiRegistration: schema: $ref: 'components.yaml#/components/schemas/RestDmiPluginRegistration' responses: - 204: + 200: $ref: 'components.yaml#/components/responses/NoContent' 400: $ref: 'components.yaml#/components/responses/BadRequest' @@ -40,4 +40,7 @@ updateDmiRegistration: 403: $ref: 'components.yaml#/components/responses/Forbidden' 500: - $ref: 'components.yaml#/components/responses/InternalServerError' + content: + application/json: + schema: + $ref: 'components.yaml#/components/schemas/DmiPluginRegistrationErrorResponse' diff --git a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java index c9d26f2a5..e9a69b305 100755 --- a/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java +++ b/cps-ncmp-rest/src/main/java/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryController.java @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021 Bell Canada + * Copyright (C) 2021-2022 Bell Canada * Modifications Copyright (C) 2022 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,10 +21,17 @@ package org.onap.cps.ncmp.rest.controller; +import java.util.List; +import java.util.stream.Collectors; import javax.validation.Valid; import lombok.RequiredArgsConstructor; import org.onap.cps.ncmp.api.NetworkCmProxyDataService; +import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse; +import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.Status; +import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse; import org.onap.cps.ncmp.rest.api.NetworkCmProxyInventoryApi; +import org.onap.cps.ncmp.rest.model.CmHandlerRegistrationErrorResponse; +import org.onap.cps.ncmp.rest.model.DmiPluginRegistrationErrorResponse; import org.onap.cps.ncmp.rest.model.RestDmiPluginRegistration; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -41,14 +48,57 @@ public class NetworkCmProxyInventoryController implements NetworkCmProxyInventor /** * Update DMI Plugin Registration (used for first registration also). + * * @param restDmiPluginRegistration the registration data */ @Override - public ResponseEntity updateDmiPluginRegistration( + public ResponseEntity updateDmiPluginRegistration( final @Valid RestDmiPluginRegistration restDmiPluginRegistration) { - networkCmProxyDataService.updateDmiRegistrationAndSyncModule( - ncmpRestInputMapper.toDmiPluginRegistration(restDmiPluginRegistration)); - return new ResponseEntity<>(HttpStatus.NO_CONTENT); + final DmiPluginRegistrationResponse dmiPluginRegistrationResponse = + networkCmProxyDataService.updateDmiRegistrationAndSyncModule( + ncmpRestInputMapper.toDmiPluginRegistration(restDmiPluginRegistration)); + final DmiPluginRegistrationErrorResponse failedRegistrationErrorResponse = + getFailureRegistrationResponse(dmiPluginRegistrationResponse); + return allRegistrationsSuccessful(failedRegistrationErrorResponse) + ? new ResponseEntity<>(HttpStatus.OK) + : new ResponseEntity<>(failedRegistrationErrorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } + + private boolean allRegistrationsSuccessful( + final DmiPluginRegistrationErrorResponse dmiPluginRegistrationErrorResponse) { + return dmiPluginRegistrationErrorResponse.getFailedCreatedCmHandles().isEmpty() + && dmiPluginRegistrationErrorResponse.getFailedUpdatedCmHandles().isEmpty() + && dmiPluginRegistrationErrorResponse.getFailedRemovedCmHandles().isEmpty(); + + } + + private DmiPluginRegistrationErrorResponse getFailureRegistrationResponse( + final DmiPluginRegistrationResponse dmiPluginRegistrationResponse) { + final var dmiPluginRegistrationErrorResponse = new DmiPluginRegistrationErrorResponse(); + dmiPluginRegistrationErrorResponse.setFailedCreatedCmHandles( + getFailedResponses(dmiPluginRegistrationResponse.getCreatedCmHandles())); + dmiPluginRegistrationErrorResponse.setFailedUpdatedCmHandles( + getFailedResponses(dmiPluginRegistrationResponse.getUpdatedCmHandles())); + dmiPluginRegistrationErrorResponse.setFailedRemovedCmHandles( + getFailedResponses(dmiPluginRegistrationResponse.getRemovedCmHandles())); + + return dmiPluginRegistrationErrorResponse; + } + + private List getFailedResponses( + final List cmHandleRegistrationResponseList) { + return cmHandleRegistrationResponseList.stream() + .filter(cmHandleRegistrationResponse -> cmHandleRegistrationResponse.getStatus() == Status.FAILURE) + .map(this::toCmHandleRegistrationErrorResponse) + .collect(Collectors.toList()); + } + + private CmHandlerRegistrationErrorResponse toCmHandleRegistrationErrorResponse( + final CmHandleRegistrationResponse registrationResponse) { + return new CmHandlerRegistrationErrorResponse() + .cmHandle(registrationResponse.getCmHandle()) + .errorCode(registrationResponse.getRegistrationError().errorCode) + .errorText(registrationResponse.getErrorText()); } } diff --git a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy index 9b1c2e87c..30b6beb37 100644 --- a/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy +++ b/cps-ncmp-rest/src/test/groovy/org/onap/cps/ncmp/rest/controller/NetworkCmProxyInventoryControllerSpec.groovy @@ -1,6 +1,6 @@ /* * ============LICENSE_START======================================================= - * Copyright (C) 2021 Bell Canada + * Copyright (C) 2021-2022 Bell Canada * Modifications Copyright (C) 2021-2022 Nordix Foundation * ================================================================================ * Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,11 @@ package org.onap.cps.ncmp.rest.controller import com.fasterxml.jackson.databind.ObjectMapper import org.onap.cps.TestUtils import org.onap.cps.ncmp.api.NetworkCmProxyDataService +import org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse import org.onap.cps.ncmp.api.models.DmiPluginRegistration +import org.onap.cps.ncmp.api.models.DmiPluginRegistrationResponse +import org.onap.cps.ncmp.rest.model.CmHandlerRegistrationErrorResponse +import org.onap.cps.ncmp.rest.model.DmiPluginRegistrationErrorResponse import org.onap.cps.ncmp.rest.model.RestDmiPluginRegistration import org.onap.cps.utils.JsonObjectMapper import org.spockframework.spring.SpringBean @@ -36,6 +40,9 @@ import org.springframework.http.HttpStatus import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc import spock.lang.Specification + +import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_ALREADY_EXIST +import static org.onap.cps.ncmp.api.models.CmHandleRegistrationResponse.RegistrationError.CM_HANDLE_DOES_NOT_EXIST import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post @WebMvcTest(NetworkCmProxyInventoryController) @@ -58,7 +65,7 @@ class NetworkCmProxyInventoryControllerSpec extends Specification { @Value('${rest.api.ncmp-inventory-base-path}/v1') def ncmpBasePathV1 - def 'Dmi plugin registration #scenario' () { + def 'Dmi plugin registration #scenario'() { given: 'a dmi plugin registration with #scenario' def jsonData = TestUtils.getResourceFileContent(dmiRegistrationJson) and: 'the expected rest input as an object' @@ -72,9 +79,9 @@ class NetworkCmProxyInventoryControllerSpec extends Specification { .content(jsonData) ).andReturn().response then: 'the converted object is forwarded to the registration service' - 1 * mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(mockDmiPluginRegistration) + 1 * mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(mockDmiPluginRegistration) >> new DmiPluginRegistrationResponse() and: 'response status is no content' - response.status == HttpStatus.NO_CONTENT.value() + response.status == HttpStatus.OK.value() where: 'the following registration json is used' scenario | dmiRegistrationJson 'multiple services, added, updated and removed cm handles and many properties' | 'dmi_registration_all_singing_and_dancing.json' @@ -82,7 +89,7 @@ class NetworkCmProxyInventoryControllerSpec extends Specification { 'without any properties' | 'dmi_registration_without_properties.json' } - def 'Dmi plugin registration with invalid json' () { + def 'Dmi plugin registration with invalid json'() { given: 'a dmi plugin registration with #scenario' def jsonDataWithUndefinedDataLabel = '{"notAdmiPlugin":""}' when: 'post request is performed & registration is called with correct DMI plugin information' @@ -95,4 +102,74 @@ class NetworkCmProxyInventoryControllerSpec extends Specification { response.status == HttpStatus.BAD_REQUEST.value() } + def 'DMI Registration: All cm-handles operations processed successfully.'() { + given: 'a dmi plugin registration' + def dmiRegistrationRequest = '{}' + and: 'service can register cm-handles successfully' + def dmiRegistrationResponse = new DmiPluginRegistrationResponse( + createdCmHandles: [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-1')], + updatedCmHandles: [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-2')], + removedCmHandles: [CmHandleRegistrationResponse.createSuccessResponse('cm-handle-3')] + ) + mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(*_) >> dmiRegistrationResponse + when: 'registration endpoint is invoked' + def response = mvc.perform( + post("$ncmpBasePathV1/ch") + .contentType(MediaType.APPLICATION_JSON) + .content(dmiRegistrationRequest) + ).andReturn().response + then: 'response status is ok' + response.status == HttpStatus.OK.value() + and: 'the response body is empty' + response.getContentAsString() == '' + + } + + def 'DMI Registration Error Handling: #scenario.'() { + given: 'a dmi plugin registration' + def dmiRegistrationRequest = '{}' + and: '#scenario: service failed to register few cm-handle' + def dmiRegistrationResponse = new DmiPluginRegistrationResponse( + createdCmHandles: [createCmHandleResponse], + updatedCmHandles: [updateCmHandleResponse], + removedCmHandles: [removeCmHandleResponse] + ) + mockNetworkCmProxyDataService.updateDmiRegistrationAndSyncModule(*_) >> dmiRegistrationResponse + when: 'registration endpoint is invoked' + def response = mvc.perform( + post("$ncmpBasePathV1/ch") + .contentType(MediaType.APPLICATION_JSON) + .content(dmiRegistrationRequest) + ).andReturn().response + then: 'request status is internal server error' + response.status == HttpStatus.INTERNAL_SERVER_ERROR.value() + and: 'the response body is in the expected format' + def responseBody = jsonObjectMapper.convertJsonString(response.getContentAsString(), DmiPluginRegistrationErrorResponse) + and: 'contains only the failure responses' + responseBody.getFailedCreatedCmHandles() == expectedFailedCreatedCmHandle + responseBody.getFailedUpdatedCmHandles() == expectedFailedUpdateCmHandle + responseBody.getFailedRemovedCmHandles() == expectedFailedRemovedCmHandle + where: + scenario | createCmHandleResponse | updateCmHandleResponse | removeCmHandleResponse || expectedFailedCreatedCmHandle | expectedFailedUpdateCmHandle | expectedFailedRemovedCmHandle + 'only create failed' | failedResponse('cm-handle-1') | successResponse('cm-handle-2') | successResponse('cm-handle-3') || [failedRestResponse('cm-handle-1')] | [] | [] + 'only update failed' | successResponse('cm-handle-1') | failedResponse('cm-handle-2') | successResponse('cm-handle-3') || [] | [failedRestResponse('cm-handle-2')] | [] + 'only delete failed' | successResponse('cm-handle-1') | successResponse('cm-handle-2') | failedResponse('cm-handle-3') || [] | [] | [failedRestResponse('cm-handle-3')] + 'all three failed' | failedResponse('cm-handle-1') | failedResponse('cm-handle-2') | failedResponse('cm-handle-3') || [failedRestResponse('cm-handle-1')] | [failedRestResponse('cm-handle-2')] | [failedRestResponse('cm-handle-3')] + 'create update failed' | failedResponse('cm-handle-1') | failedResponse('cm-handle-2') | successResponse('cm-handle-3') || [failedRestResponse('cm-handle-1')] | [failedRestResponse('cm-handle-2')] | [] + 'create delete failed' | failedResponse('cm-handle-1') | successResponse('cm-handle-2') | failedResponse('cm-handle-3') || [failedRestResponse('cm-handle-1')] | [] | [failedRestResponse('cm-handle-3')] + 'update delete failed' | successResponse('cm-handle-1') | failedResponse('cm-handle-2') | failedResponse('cm-handle-3') || [] | [failedRestResponse('cm-handle-2')] | [failedRestResponse('cm-handle-3')] + } + + def failedRestResponse(cmHandle) { + return new CmHandlerRegistrationErrorResponse('cmHandle': cmHandle, 'errorCode': '00', 'errorText': 'Failed') + } + + def failedResponse(cmHandle) { + return CmHandleRegistrationResponse.createFailureResponse(cmHandle, new RuntimeException("Failed")) + } + + def successResponse(cmHandle) { + return CmHandleRegistrationResponse.createSuccessResponse(cmHandle) + } + } diff --git a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java index ce2f3e66a..8a3d26414 100644 --- a/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java +++ b/cps-ncmp-service/src/main/java/org/onap/cps/ncmp/api/models/DmiPluginRegistrationResponse.java @@ -20,6 +20,7 @@ package org.onap.cps.ncmp.api.models; +import java.util.Collections; import java.util.List; import lombok.Data; import lombok.NoArgsConstructor; @@ -27,7 +28,7 @@ import lombok.NoArgsConstructor; @Data @NoArgsConstructor public class DmiPluginRegistrationResponse { - private List createdCmHandles; - private List updatedCmHandles; - private List removedCmHandles; + private List createdCmHandles = Collections.emptyList(); + private List updatedCmHandles = Collections.emptyList(); + private List removedCmHandles = Collections.emptyList(); } \ No newline at end of file diff --git a/csit/tests/cps-model-sync/cps-model-sync.robot b/csit/tests/cps-model-sync/cps-model-sync.robot index dfad94861..7de1f3a1b 100644 --- a/csit/tests/cps-model-sync/cps-model-sync.robot +++ b/csit/tests/cps-model-sync/cps-model-sync.robot @@ -42,7 +42,7 @@ Register data node and sync modules. ${uri}= Set Variable ${ncmpInventoryBasePath}/v1/ch ${headers}= Create Dictionary Content-Type=application/json Authorization=${auth} ${response}= POST On Session CPS_URL ${uri} headers=${headers} data=${jsonDataCreate} - Should Be Equal As Strings ${response.status_code} 204 + Should Be Equal As Strings ${response.status_code} 200 Get CM Handle details and confirm it has been registered. ${uri}= Set Variable ${ncmpBasePath}/v1/ch/PNFDemo @@ -61,7 +61,7 @@ Update data node and sync modules. ${uri}= Set Variable ${ncmpInventoryBasePath}/v1/ch ${headers}= Create Dictionary Content-Type=application/json Authorization=${auth} ${response}= POST On Session CPS_URL ${uri} headers=${headers} data=${jsonDataUpdate} - Should Be Equal As Strings ${response.status_code} 204 + Should Be Equal As Strings ${response.status_code} 200 Get CM Handle details and confirm it has been updated. ${uri}= Set Variable ${ncmpBasePath}/v1/ch/PNFDemo -- cgit 1.2.3-korg