diff --git a/.gitignore b/.gitignore index 6e7eb1f..8f054d0 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,4 @@ out/ ### secrets skip for token generation ### /openshift/*.key /openshift/*.p12 +/src/main/generated/ diff --git a/build.gradle b/build.gradle index 2707dda..f5a69b9 100644 --- a/build.gradle +++ b/build.gradle @@ -84,10 +84,13 @@ dependencies { implementation 'org.apache.commons:commons-collections4:4.5.0' implementation 'org.openapitools:jackson-databind-nullable:0.2.6' implementation 'nl.basjes.codeowners:codeowners-reader:1.9.0' + implementation 'org.mapstruct:mapstruct:1.5.5.Final' // Development dependencies - implementation 'org.projectlombok:lombok' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' annotationProcessor 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok-mapstruct-binding:0.2.0' developmentOnly 'org.springframework.boot:spring-boot-devtools' // Testing dependencies diff --git a/openapi/openapi-component_catalog-v1.0.0.yaml b/openapi/openapi-component_catalog-v1.0.0.yaml index deb8851..4813ab4 100644 --- a/openapi/openapi-component_catalog-v1.0.0.yaml +++ b/openapi/openapi-component_catalog-v1.0.0.yaml @@ -48,12 +48,6 @@ paths: required: true schema: type: string - - name: accessToken - in: query - description: access token for azure queries. - required: true - schema: - type: string responses: "200": description: A list of Project Component Information @@ -81,6 +75,50 @@ paths: application/json: schema: $ref: '#/components/schemas/RestErrorMessage' + /project/{projectKey}/component/{componentId}: + get: + tags: + - Project-components + summary: Returns the extended information of a project component given both its project key and component ID in the Bitbucket repository. + operationId: getProjectComponentById + parameters: + - name: projectKey + in: path + description: project key. + required: true + schema: + type: string + - name: componentId + in: path + description: component ID. + required: true + schema: + type: string + responses: + "200": + description: The extended information of a project component. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectComponentExtendedInfo' + "401": + description: Invalid client token on the request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "403": + description: Insufficient permissions for the client to access the resource. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' /catalog-descriptors: get: tags: @@ -357,12 +395,6 @@ paths: required: true schema: type: string - - name: accessToken - in: query - description: access token for azure queries. - required: true - schema: - type: string - name: sortByTitle in: query description: Sort the returned CatalogItems by title, either in ascending or descending order. @@ -436,12 +468,6 @@ paths: required: true schema: type: string - - name: accessToken - in: query - description: access token for azure queries. - required: true - schema: - type: string responses: "200": description: The CatalogItem. @@ -871,6 +897,39 @@ components: componentUrl: type: string example: 'https://bitbucket.com/projects/CATALOGS/repos/project-components/browse/projects' + ProjectComponentParameter: + properties: + name: + type: string + example: 'a name' + values: + type: array + items: + type: string + example: + - 'value 1' + - 'value 2' + ProjectComponentExtendedInfo: + properties: + componentId: + type: string + example: 'edpc-4132-v2' + catalogItemId: + type: string + example: 'edpc-4132-v2' + catalogItemRef: + type: string + example: 'edpc-4132-v2' + status: + type: string + example: 'CREATING' + componentUrl: + type: string + example: 'https://bitbucket.com/projects/CATALOGS/repos/project-components/browse/projects' + parameters: + type: array + items: + $ref: '#/components/schemas/ProjectComponentParameter' CatalogDescriptor: properties: id: @@ -1224,12 +1283,6 @@ components: example: "https://bitbucket.com/projects/DEVSTACK/repos/devstack-component-catalog" nullable: true - accessToken: - type: string - description: the access token to be used to get azure groups - example: "some-access-token" - nullable: false - parameters: type: array description: List of name/value string parameters. @@ -1282,4 +1335,4 @@ components: type: string example: - "production" - - "staging" \ No newline at end of file + - "staging" diff --git a/src/main/java/org/opendevstack/component_provisioner/ComponentProvisionerApplication.java b/src/main/java/org/opendevstack/component_provisioner/ComponentProvisionerApplication.java index 5aa2b71..b7dbeaf 100644 --- a/src/main/java/org/opendevstack/component_provisioner/ComponentProvisionerApplication.java +++ b/src/main/java/org/opendevstack/component_provisioner/ComponentProvisionerApplication.java @@ -24,6 +24,7 @@ "org.opendevstack.component_provisioner.server.security", "org.opendevstack.component_provisioner.config", "org.opendevstack.component_provisioner.server.services", + "org.opendevstack.component_provisioner.server.mappers", "org.opendevstack.component_provisioner.client.awx.v2" }, nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class diff --git a/src/main/java/org/opendevstack/component_provisioner/config/ControllerExceptionHandler.java b/src/main/java/org/opendevstack/component_provisioner/config/ControllerExceptionHandler.java index 57db907..51318c5 100644 --- a/src/main/java/org/opendevstack/component_provisioner/config/ControllerExceptionHandler.java +++ b/src/main/java/org/opendevstack/component_provisioner/config/ControllerExceptionHandler.java @@ -3,6 +3,7 @@ import org.opendevstack.component_provisioner.server.controllers.exceptions.*; import org.opendevstack.component_provisioner.server.model.RestErrorMessage; import lombok.extern.slf4j.Slf4j; +import org.opendevstack.component_provisioner.server.services.exceptions.InvalidIdException; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -61,6 +62,16 @@ public ResponseEntity handleSlugNotFoundException(SlugNotFound return defaultErrResponse(ex, HttpStatus.NOT_FOUND); } + @ExceptionHandler(ComponentNotFoundException.class) + public ResponseEntity handleComponentNotFoundException(ComponentNotFoundException ex) { + return defaultErrResponse(ex, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(InvalidIdException.class) + public ResponseEntity handleInvalidIdException(InvalidIdException ex) { + return defaultErrResponse(ex, HttpStatus.BAD_REQUEST); + } + private static ResponseEntity defaultErrResponse(Exception ex, HttpStatus errStatus) { // Explicitly setting MediaType.APPLICATION_JSON contentType is required, // due to clients sending miscellaneous Accept headers on the request, diff --git a/src/main/java/org/opendevstack/component_provisioner/server/controllers/exceptions/ComponentNotFoundException.java b/src/main/java/org/opendevstack/component_provisioner/server/controllers/exceptions/ComponentNotFoundException.java new file mode 100644 index 0000000..69a03d4 --- /dev/null +++ b/src/main/java/org/opendevstack/component_provisioner/server/controllers/exceptions/ComponentNotFoundException.java @@ -0,0 +1,7 @@ +package org.opendevstack.component_provisioner.server.controllers.exceptions; + +public class ComponentNotFoundException extends RuntimeException { + public ComponentNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java index 732f1d7..fd9203b 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacade.java @@ -112,9 +112,12 @@ private String resolveCatalogItemId(String accessToken, } public void deleteProvisioningStatus(String projectKey, String componentId, String accessToken) { - provisionService.deleteProvisioningStatus(projectKey, componentId, accessToken); + var projectComponents = componentCatalogService.getProjectComponentExtendedInfo(projectKey, componentId, accessToken); + + provisionService.deleteProvisioningStatus(projectKey, componentId, projectComponents, accessToken); } + public void validate(String projectKey, String status, NotifyProvisioningStatusUpdateRequest request) { validate(projectKey, status); if (StringUtils.isNotBlank(request.getCatalogItemId()) && StringUtils.isNotBlank(request.getCatalogItemSlug())) { diff --git a/src/main/java/org/opendevstack/component_provisioner/server/mappers/ProvisioningStatusUpdateRequestParametersInnerMapper.java b/src/main/java/org/opendevstack/component_provisioner/server/mappers/ProvisioningStatusUpdateRequestParametersInnerMapper.java new file mode 100644 index 0000000..5145b89 --- /dev/null +++ b/src/main/java/org/opendevstack/component_provisioner/server/mappers/ProvisioningStatusUpdateRequestParametersInnerMapper.java @@ -0,0 +1,14 @@ +package org.opendevstack.component_provisioner.server.mappers; + +import org.mapstruct.Mapper; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentParameter; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProvisioningStatusUpdateRequestParametersInner; + +@Mapper(componentModel = "spring") +public interface ProvisioningStatusUpdateRequestParametersInnerMapper { + + ProvisioningStatusUpdateRequestParametersInner toTarget( + ProjectComponentParameter source + ); + +} diff --git a/src/main/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogService.java b/src/main/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogService.java index 26c8c59..e24124e 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogService.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogService.java @@ -118,7 +118,6 @@ public void notifyComponentCatalogProvisionStarts(String projectKey, .componentId(componentId) .catalogItemId(catalogItemId) .componentUrl(componentUrl) - .accessToken(accessToken) .parameters(obfuscatedParameters) .build(); @@ -134,7 +133,14 @@ public List getProjectComponents(String projectKey, String var auth = (HttpBearerAuth) componentCatalogApiClient.getAuthentication("bearerAuth"); auth.setBearerToken(accessToken); - return projectComponentsApi.getProjectComponents(projectKey, accessToken); + return projectComponentsApi.getProjectComponents(projectKey); + } + + public ProjectComponentExtendedInfo getProjectComponentExtendedInfo(String projectKey, String componentId, String accessToken) { + var auth = (HttpBearerAuth) componentCatalogApiClient.getAuthentication("bearerAuth"); + auth.setBearerToken(accessToken); + + return projectComponentsApi.getProjectComponentById(projectKey, componentId); } @Cacheable @@ -142,7 +148,7 @@ public CatalogItem getCatalogItem(String accessToken, String catalogItemId, Stri var apiClient = apiClientsBuilder.componentCatalogApiClient(accessToken, componentCatalogServiceProps.getBaseRestUrl().toString()); var catalogItemsApi = apiClientsBuilder.catalogItemsApi(apiClient); - var catalogItem = catalogItemsApi.getCatalogItemByIdForProjectKey(catalogItemId, projectKey, accessToken); + var catalogItem = catalogItemsApi.getCatalogItemByIdForProjectKey(catalogItemId, projectKey); log.debug("Retrieved catalog item with id {} for project key {}: {}", catalogItemId, projectKey, catalogItem); diff --git a/src/main/java/org/opendevstack/component_provisioner/server/services/ProvisionService.java b/src/main/java/org/opendevstack/component_provisioner/server/services/ProvisionService.java index 11509a7..c423c2f 100644 --- a/src/main/java/org/opendevstack/component_provisioner/server/services/ProvisionService.java +++ b/src/main/java/org/opendevstack/component_provisioner/server/services/ProvisionService.java @@ -1,13 +1,24 @@ package org.opendevstack.component_provisioner.server.services; import lombok.AllArgsConstructor; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProvisioningDeleteRequest; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProvisioningStatusUpdateRequest; import lombok.extern.slf4j.Slf4j; +import org.apache.logging.log4j.util.Strings; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.*; import org.opendevstack.component_provisioner.config.ApplicationPropertiesConfiguration; import org.opendevstack.component_provisioner.server.controllers.model.ProjectComponentStatus; +import org.opendevstack.component_provisioner.server.mappers.ProvisioningStatusUpdateRequestParametersInnerMapper; +import org.opendevstack.component_provisioner.server.services.exceptions.InvalidIdException; import org.springframework.stereotype.Service; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.opendevstack.component_provisioner.server.services.common.IdEncoderDecoder.idDecode; +import static org.opendevstack.component_provisioner.server.services.common.IdEncoderDecoder.idEncode; + @Service @Slf4j @AllArgsConstructor @@ -15,6 +26,7 @@ public class ProvisionService { private final ApiClientsBuilder apiClientsBuilder; private final ApplicationPropertiesConfiguration.ComponentCatalogServiceProps componentCatalogServiceProps; + private final ProvisioningStatusUpdateRequestParametersInnerMapper provisioningStatusUpdateRequestParametersInnerMapper; public void notifyProvisioningStatusUpdate(String projectKey, ProjectComponentStatus status, String componentId, String catalogItemId, String componentUrl, String accessToken) { @@ -26,7 +38,6 @@ public void notifyProvisioningStatusUpdate(String projectKey, ProjectComponentSt .componentId(componentId) .catalogItemId(catalogItemId) .componentUrl(componentUrl) - .accessToken(accessToken) .build(); log.debug("Calling provisionerActionsApi.notifyProvisioningStatusUpdatePartially. ProjectKey: {}, status: {}, notifyProvisioningCompletedRequest: {}", @@ -35,15 +46,74 @@ public void notifyProvisioningStatusUpdate(String projectKey, ProjectComponentSt provisionerActionsApi.notifyProvisioningStatusUpdatePartially(projectKey, status.name(), notifyProvisioningCompletedRequest); } - public void deleteProvisioningStatus(String projectKey, String componentId, String accessToken) { + public void deleteProvisioningStatus(String projectKey, + String componentId, + ProjectComponentExtendedInfo projectComponent, + String accessToken) { log.info("Deleting provisioning completed. Project Key: {}, componentId: {}", projectKey, componentId); + var catalogItemId = composeCatalogItemId(projectComponent); + + var apiClient = apiClientsBuilder.componentCatalogApiClient(accessToken, componentCatalogServiceProps.getBaseRestUrl().toString()); + + var catalogItemsApi = apiClientsBuilder.catalogItemsApi(apiClient); + var catalogItem = catalogItemsApi.getCatalogItemById(catalogItemId); + + var parametersToSend = extractDeletionParameters(catalogItem, projectComponent); + + if (parametersToSend.isEmpty()) { + log.debug("No parameters marked as sendOnDeletion found"); + } + var provisioningDeleteRequest = ProvisioningDeleteRequest.builder() .componentId(componentId) + .parameters(parametersToSend) .build(); - var provisionerActionsApi = apiClientsBuilder.provisionerActionsApi(accessToken, componentCatalogServiceProps.getBaseRestUrl().toString()); + var provisionerActionsApi = apiClientsBuilder.provisionerActionsApi(accessToken, + componentCatalogServiceProps.getBaseRestUrl().toString()); provisionerActionsApi.deleteProvisioningStatus(projectKey, provisioningDeleteRequest); } + + private String composeCatalogItemId(ProjectComponentExtendedInfo projectComponents) { + try { + var decodedCatalogItemId = idDecode(projectComponents.getCatalogItemId()); + var decodedCatalogItemRef = idDecode(projectComponents.getCatalogItemRef()); + return idEncode(Strings.concat(decodedCatalogItemId, decodedCatalogItemRef)); + + } catch (InvalidIdException e) { + throw new RuntimeException(e); + } + } + + private List extractDeletionParameters( + CatalogItem catalogItem, + ProjectComponentExtendedInfo projectComponent) { + var projectParametersByName = + Optional.ofNullable(projectComponent.getParameters()) + .orElse(List.of()) + .stream() + .filter(p -> p.getName() != null) + .collect(Collectors.toMap( + ProjectComponentParameter::getName, + Function.identity(), (a, b) -> a + )); + + return Optional.ofNullable(catalogItem.getUserActions()) + .orElse(List.of()) + .stream() + .peek(action -> log.debug("User action found: {}", action)) + .flatMap(action -> + Optional.ofNullable(action.getParameters()) + .orElse(List.of()) + .stream() + ) + .peek(param -> log.debug("Parameter found: {}", param)) + .filter(param -> Boolean.TRUE.equals(param.getSendOnDeletion())) + .map(param -> projectParametersByName.get(param.getName())) + .filter(Objects::nonNull) + .map(provisioningStatusUpdateRequestParametersInnerMapper::toTarget) + .toList(); + } } diff --git a/src/main/java/org/opendevstack/component_provisioner/server/services/common/IdEncoderDecoder.java b/src/main/java/org/opendevstack/component_provisioner/server/services/common/IdEncoderDecoder.java new file mode 100644 index 0000000..bc531fa --- /dev/null +++ b/src/main/java/org/opendevstack/component_provisioner/server/services/common/IdEncoderDecoder.java @@ -0,0 +1,33 @@ +package org.opendevstack.component_provisioner.server.services.common; + + +import org.opendevstack.component_provisioner.server.services.exceptions.InvalidIdException; + +import java.util.Base64; + +public class IdEncoderDecoder { + + private IdEncoderDecoder() { + // Hide the implicit public constructor + } + + public static String idEncode(String path) { + return Base64.getUrlEncoder().encodeToString(path.getBytes()); + } + + public static String nullableIdEncode(String path) { + return path == null ? null : idEncode(path); + } + + public static String idDecode(String id) throws InvalidIdException { + try { + return new String(Base64.getUrlDecoder().decode(id)); + } catch (Exception e) { + throw new InvalidIdException(id); + } + } + + public static String nullableIdDecode(String id) throws InvalidIdException { + return id == null ? null : idDecode(id); + } +} diff --git a/src/main/java/org/opendevstack/component_provisioner/server/services/exceptions/InvalidIdException.java b/src/main/java/org/opendevstack/component_provisioner/server/services/exceptions/InvalidIdException.java new file mode 100644 index 0000000..4b42544 --- /dev/null +++ b/src/main/java/org/opendevstack/component_provisioner/server/services/exceptions/InvalidIdException.java @@ -0,0 +1,12 @@ +package org.opendevstack.component_provisioner.server.services.exceptions; + +public class InvalidIdException extends Exception { + + public InvalidIdException(String id) { + this("Invalid id: " + id, null); + } + + public InvalidIdException(String id, Exception cause) { + super("Invalid id: " + id, cause); + } +} diff --git a/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java b/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java index 792fb96..d702822 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/facade/ProvisionResultsApiFacadeTest.java @@ -8,6 +8,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.opendevstack.component_catalog.client.projects_info_service.v1_0_0.model.ProjectInfo; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentInfo; import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentInfoMother; import org.opendevstack.component_provisioner.server.controllers.exceptions.InvalidRestEntityException; @@ -38,6 +39,7 @@ import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -320,15 +322,18 @@ void givenOnlyCatalogItemSlug_whenValidateIsCalled_thenDoesNotThrow() { } @Test - void givenAValidStatusAndRequest_whenValidateIsCalled_thenDoesNotThrow() { + void givenProjectKeyAndStatusAndRequestWithBothIdAndSlug_whenValidateIsCalled_thenThrowsInvalidRestEntityException() { // given var projectKey = "PRJ"; var status = ProjectComponentStatus.CREATED.name(); var request = new NotifyProvisioningStatusUpdateRequest(); request.setCatalogItemId("ID"); + request.setCatalogItemSlug("SLUG"); // when / then - assertDoesNotThrow(() -> facade.validate(projectKey, status, request)); + assertThatThrownBy(() -> facade.validate(projectKey, status, request)) + .isInstanceOf(InvalidRestEntityException.class) + .hasMessage("Both catalogItemId and catalogItemSlug cannot be defined at the same time."); } @Test @@ -338,11 +343,17 @@ void givenAProjectKeyAndAComponentId_whenDeleteProvisioningStatusIsCalled_thenDe var componentId = "CID"; var accessToken = "token"; + var projectComponent = new ProjectComponentExtendedInfo(); + projectComponent.setComponentId(componentId); + + when(componentCatalogService.getProjectComponentExtendedInfo(projectKey, componentId, accessToken)) + .thenReturn(projectComponent); + // when facade.deleteProvisioningStatus(projectKey, componentId, accessToken); // then - verify(provisionService).deleteProvisioningStatus(projectKey, componentId, accessToken); + verify(provisionService).deleteProvisioningStatus(projectKey, componentId, projectComponent, accessToken); } @Test diff --git a/src/test/java/org/opendevstack/component_provisioner/server/model/ProjectComponentExtendedInfoMother.java b/src/test/java/org/opendevstack/component_provisioner/server/model/ProjectComponentExtendedInfoMother.java new file mode 100644 index 0000000..91a0fb9 --- /dev/null +++ b/src/test/java/org/opendevstack/component_provisioner/server/model/ProjectComponentExtendedInfoMother.java @@ -0,0 +1,61 @@ +package org.opendevstack.component_provisioner.server.model; + +import java.util.Collections; +import java.util.List; + +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentExtendedInfo; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProjectComponentParameter; + +public final class ProjectComponentExtendedInfoMother { + + private ProjectComponentExtendedInfoMother() { + // Evita instanciación + } + + /** + * Objeto completamente válido con valores por defecto + */ + public static ProjectComponentExtendedInfo valid() { + return ProjectComponentExtendedInfo.builder() + .componentId("component-id") + .catalogItemId("aHR0cDovL2JpdGJ1Y2tldC10ZXN0LmNvbQ") + .catalogItemRef("L3JlZmVyZW5jZT9wYXJhbT0xMA") + .status("CREATED") + .componentUrl("https://example.com/component") + .parameters(Collections.emptyList()) + .build(); + } + + /** + * Variante simple pasando solo los campos relevantes al test + */ + public static ProjectComponentExtendedInfo of( + String componentId, + String status + ) { + return ProjectComponentExtendedInfo.builder() + .componentId(componentId) + .status(status) + .catalogItemId("aHR0cDovL2JpdGJ1Y2tldC10ZXN0LmNvbQ") + .catalogItemRef("L3JlZmVyZW5jZT9wYXJhbT0xMA") + .componentUrl("https://example.com/component") + .parameters(Collections.emptyList()) + .build(); + } + + /** + * Variante si necesitas parámetros + */ + public static ProjectComponentExtendedInfo withParameters( + List parameters + ) { + return ProjectComponentExtendedInfo.builder() + .componentId("component-id") + .catalogItemId("aHR0cDovL2JpdGJ1Y2tldC10ZXN0LmNvbQ") + .catalogItemRef("L3JlZmVyZW5jZT9wYXJhbT0xMA") + .status("CREATED") + .componentUrl("https://example.com/component") + .parameters(parameters) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogServiceTest.java b/src/test/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogServiceTest.java index bcee390..67da006 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogServiceTest.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/services/ComponentCatalogServiceTest.java @@ -333,7 +333,7 @@ void givenValidInput_whenGetCatalogItemIsCalled_thenCatalogItemIsReturned() thro when(apiClientsBuilder.catalogItemsApi(componentCatalogApiClient)) .thenReturn(catalogItemsApi); when(catalogItemsApi.getCatalogItemByIdForProjectKey( - catalogItemId, projectKey, accessToken)) + catalogItemId, projectKey)) .thenReturn(expectedCatalogItem); // when @@ -348,7 +348,7 @@ void givenValidInput_whenGetCatalogItemIsCalled_thenCatalogItemIsReturned() thro verify(apiClientsBuilder) .catalogItemsApi(componentCatalogApiClient); verify(catalogItemsApi) - .getCatalogItemByIdForProjectKey(catalogItemId, projectKey, accessToken); + .getCatalogItemByIdForProjectKey(catalogItemId, projectKey); verifyNoMoreInteractions(catalogItemsApi); verifyNoInteractions( @@ -368,7 +368,7 @@ void givenValidInput_whenGetProjectComponentsIsCalled_thenProjectComponentsAreRe when(componentCatalogApiClient.getAuthentication("bearerAuth")).thenReturn(auth); List expectedComponents = List.of(); - when(projectComponentsApi.getProjectComponents(projectKey, accessToken)).thenReturn(expectedComponents); + when(projectComponentsApi.getProjectComponents(projectKey)).thenReturn(expectedComponents); // when List result = componentCatalogService.getProjectComponents(projectKey, accessToken); @@ -376,7 +376,7 @@ void givenValidInput_whenGetProjectComponentsIsCalled_thenProjectComponentsAreRe // then assertThat(result).isSameAs(expectedComponents); verify(auth).setBearerToken(accessToken); - verify(projectComponentsApi).getProjectComponents(projectKey, accessToken); + verify(projectComponentsApi).getProjectComponents(projectKey); } @Test diff --git a/src/test/java/org/opendevstack/component_provisioner/server/services/ProvisionServiceTest.java b/src/test/java/org/opendevstack/component_provisioner/server/services/ProvisionServiceTest.java index efa30de..bd2237b 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/services/ProvisionServiceTest.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/services/ProvisionServiceTest.java @@ -5,14 +5,21 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.opendevstack.component_provisioner.client.component_catalog.v1.ApiClient; +import org.opendevstack.component_provisioner.client.component_catalog.v1.api.CatalogItemsApi; import org.opendevstack.component_provisioner.client.component_catalog.v1.api.ProvisionerActionsApi; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProvisioningDeleteRequest; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProvisioningStatusUpdateRequest; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.*; import org.opendevstack.component_provisioner.config.ApplicationPropertiesConfiguration; import org.opendevstack.component_provisioner.server.controllers.model.ProjectComponentStatus; +import org.opendevstack.component_provisioner.server.mappers.ProvisioningStatusUpdateRequestParametersInnerMapper; +import org.opendevstack.component_provisioner.server.model.ProjectComponentExtendedInfoMother; +import org.opendevstack.component_provisioner.server.services.exceptions.InvalidIdException; import java.net.URL; +import java.util.List; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -28,6 +35,15 @@ class ProvisionServiceTest { @Mock private ProvisionerActionsApi provisionerActionsApi; + @Mock + private ApiClient apiClient; + + @Mock + private CatalogItemsApi catalogItemsApi; + + @Mock + private ProvisioningStatusUpdateRequestParametersInnerMapper provisioningStatusUpdateRequestParametersInnerMapper; + @InjectMocks private ProvisionService provisionService; @@ -53,12 +69,26 @@ void givenAProjectKeyAndStatusAndComponentIdAndCatalogItemIdAndComponentUrlAndAc .componentId(componentId) .catalogItemId(catalogItemId) .componentUrl(componentUrl) - .accessToken(accessToken) .build(); verify(provisionerActionsApi).notifyProvisioningStatusUpdatePartially(projectKey, status.name(), expectedRequest); } + @Test + void givenInvalidIdInProjectComponent_whenDeleteProvisioningStatusIsCalled_thenThrowsRuntimeException() throws Exception { + // given + var projectKey = "PRJ"; + var componentId = "CID"; + var accessToken = "token"; + var projectComponent = new ProjectComponentExtendedInfo(); + projectComponent.setCatalogItemId("!!!"); // Invalid base64 + + // when / then + assertThatThrownBy(() -> provisionService.deleteProvisioningStatus(projectKey, componentId, projectComponent, accessToken)) + .isInstanceOf(RuntimeException.class) + .hasCauseInstanceOf(InvalidIdException.class); + } + @Test void givenAProjectKeyAndComponentIdAndAccessToken_whenDeleteProvisioningStatusIsCalled_thenInvokesProvisionerActionsApi() throws Exception { // given @@ -66,16 +96,36 @@ void givenAProjectKeyAndComponentIdAndAccessToken_whenDeleteProvisioningStatusIs var componentId = "CID"; var accessToken = "token"; var baseUrl = "http://catalog.example.com"; + var projectComponent = ProjectComponentExtendedInfoMother.valid(); when(componentCatalogServiceProps.getBaseRestUrl()).thenReturn(new URL(baseUrl)); + when(apiClientsBuilder.componentCatalogApiClient(accessToken, baseUrl)).thenReturn(apiClient); + when(apiClientsBuilder.catalogItemsApi(apiClient)).thenReturn(catalogItemsApi); + CatalogItem catalogItem = new CatalogItem(); + catalogItem.setUserActions(List.of(CatalogItemUserAction.builder() + .parameters(List.of(CatalogItemUserActionParameter.builder() + .name("param1") + .sendOnDeletion(true) + .build())) + .build())); + when(catalogItemsApi.getCatalogItemById(any())).thenReturn(catalogItem); + when(apiClientsBuilder.provisionerActionsApi(accessToken, baseUrl)).thenReturn(provisionerActionsApi); + when(provisioningStatusUpdateRequestParametersInnerMapper.toTarget(any())) + .thenReturn(new ProvisioningStatusUpdateRequestParametersInner()); + + projectComponent.setParameters(List.of(ProjectComponentParameter.builder() + .name("param1") + .values(List.of("value1")) + .build())); // when - provisionService.deleteProvisioningStatus(projectKey, componentId, accessToken); + provisionService.deleteProvisioningStatus(projectKey, componentId, projectComponent, accessToken); // then var expectedRequest = ProvisioningDeleteRequest.builder() .componentId(componentId) + .parameters(List.of(new ProvisioningStatusUpdateRequestParametersInner())) .build(); verify(provisionerActionsApi).deleteProvisioningStatus(projectKey, expectedRequest); diff --git a/src/test/java/org/opendevstack/component_provisioner/server/services/ProvisionerServiceTest.java b/src/test/java/org/opendevstack/component_provisioner/server/services/ProvisionerServiceTest.java index 8b76fe3..3bebb26 100644 --- a/src/test/java/org/opendevstack/component_provisioner/server/services/ProvisionerServiceTest.java +++ b/src/test/java/org/opendevstack/component_provisioner/server/services/ProvisionerServiceTest.java @@ -1,8 +1,9 @@ package org.opendevstack.component_provisioner.server.services; +import org.opendevstack.component_provisioner.client.component_catalog.v1.ApiClient; +import org.opendevstack.component_provisioner.client.component_catalog.v1.api.CatalogItemsApi; import org.opendevstack.component_provisioner.client.component_catalog.v1.api.ProvisionerActionsApi; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProvisioningDeleteRequest; -import org.opendevstack.component_provisioner.client.component_catalog.v1.model.ProvisioningStatusUpdateRequest; +import org.opendevstack.component_provisioner.client.component_catalog.v1.model.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -10,7 +11,12 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.opendevstack.component_provisioner.config.ApplicationPropertiesConfiguration; import org.opendevstack.component_provisioner.server.controllers.model.ProjectComponentStatus; +import org.opendevstack.component_provisioner.server.mappers.ProvisioningStatusUpdateRequestParametersInnerMapper; +import org.opendevstack.component_provisioner.server.model.ProjectComponentExtendedInfoMother; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -28,13 +34,19 @@ class ProvisionerServiceTest { private ApplicationPropertiesConfiguration.ComponentCatalogServiceProps componentCatalogServiceProps; @Mock - private org.opendevstack.component_provisioner.client.component_catalog.v1.ApiClient apiClient; + private ApiClient apiClient; + + @Mock + private CatalogItemsApi catalogItemsApi; + + @Mock + private ProvisioningStatusUpdateRequestParametersInnerMapper provisioningStatusUpdateRequestParametersInnerMapper; @InjectMocks private ProvisionService provisionService; @Test - void givenAProvisionClient_WhenNotifyProvisioningCompleted_ThenProvisioningIsNotified() throws java.net.MalformedURLException { + void givenAProjectKeyAndStatusAndComponentIdAndCatalogItemIdAndComponentUrlAndAccessToken_whenNotifyProvisioningStatusUpdateIsCalled_thenInvokesProvisionerActionsApi() throws java.net.MalformedURLException { // given var projectKey = "projectKey"; var status = ProjectComponentStatus.CREATED; @@ -51,33 +63,54 @@ void givenAProvisionClient_WhenNotifyProvisioningCompleted_ThenProvisioningIsNot provisionService.notifyProvisioningStatusUpdate(projectKey, status, componentId, catalogItemId, componentUrl, accessToken); // then - verify(provisionerActionsApi).notifyProvisioningStatusUpdatePartially(projectKey, status.name(), ProvisioningStatusUpdateRequest.builder() + var expectedRequest = ProvisioningStatusUpdateRequest.builder() .componentId(componentId) .catalogItemId(catalogItemId) .componentUrl(componentUrl) - .accessToken(accessToken) - .build()); + .build(); + + verify(provisionerActionsApi).notifyProvisioningStatusUpdatePartially(projectKey, status.name(), expectedRequest); } @Test - void givenAProjectKey_andAComponentId_whenDeleteProvisioningStatus_thenProvisioningApiIsCalled() throws java.net.MalformedURLException { + void givenAProjectKeyAndComponentIdAndAccessToken_whenDeleteProvisioningStatusIsCalled_thenInvokesProvisionerActionsApi() throws java.net.MalformedURLException { // given var projectKey = "projectKey"; var componentId = "componentId"; var baseUrl = "http://localhost"; var accessToken = "accessToken"; - - var provisionDeleteRequest = ProvisioningDeleteRequest.builder() - .componentId(componentId) - .build(); + var projectComponent = ProjectComponentExtendedInfoMother.valid(); when(componentCatalogServiceProps.getBaseRestUrl()).thenReturn(java.net.URI.create(baseUrl).toURL()); + when(apiClientsBuilder.componentCatalogApiClient(accessToken, baseUrl)).thenReturn(apiClient); + when(apiClientsBuilder.catalogItemsApi(apiClient)).thenReturn(catalogItemsApi); + CatalogItem catalogItem = new CatalogItem(); + catalogItem.setUserActions(List.of(CatalogItemUserAction.builder() + .parameters(List.of(CatalogItemUserActionParameter.builder() + .name("param1") + .sendOnDeletion(true) + .build())) + .build())); + when(catalogItemsApi.getCatalogItemById(any())).thenReturn(catalogItem); + when(apiClientsBuilder.provisionerActionsApi(eq(accessToken), eq(baseUrl))).thenReturn(provisionerActionsApi); + when(provisioningStatusUpdateRequestParametersInnerMapper.toTarget(any())) + .thenReturn(new ProvisioningStatusUpdateRequestParametersInner()); + + projectComponent.setParameters(List.of(ProjectComponentParameter.builder() + .name("param1") + .values(List.of("value1")) + .build())); // when - provisionService.deleteProvisioningStatus(projectKey, componentId, accessToken); + provisionService.deleteProvisioningStatus(projectKey, componentId, projectComponent, accessToken); // then - verify(provisionerActionsApi).deleteProvisioningStatus(projectKey, provisionDeleteRequest); + var expectedRequest = ProvisioningDeleteRequest.builder() + .componentId(componentId) + .parameters(List.of(new ProvisioningStatusUpdateRequestParametersInner())) + .build(); + + verify(provisionerActionsApi).deleteProvisioningStatus(projectKey, expectedRequest); } } diff --git a/src/test/java/org/opendevstack/component_provisioner/server/services/common/IdEncoderDecoderTest.java b/src/test/java/org/opendevstack/component_provisioner/server/services/common/IdEncoderDecoderTest.java new file mode 100644 index 0000000..3d3fdc5 --- /dev/null +++ b/src/test/java/org/opendevstack/component_provisioner/server/services/common/IdEncoderDecoderTest.java @@ -0,0 +1,95 @@ +package org.opendevstack.component_provisioner.server.services.common; + +import org.junit.jupiter.api.Test; +import org.opendevstack.component_provisioner.server.services.exceptions.InvalidIdException; + +import java.util.Base64; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.opendevstack.component_provisioner.server.services.common.IdEncoderDecoder.*; + +class IdEncoderDecoderTest { + + @Test + void givenAString_whenIdEncodeIsCalled_thenReturnsBase64EncodedString() { + // given + String input = "test-string"; + + // when + String result = idEncode(input); + + // then + assertThat(result).isEqualTo(Base64.getUrlEncoder().encodeToString(input.getBytes())); + } + + @Test + void givenANullString_whenNullableIdEncodeIsCalled_thenReturnsNull() { + // given + String input = null; + + // when + String result = nullableIdEncode(input); + + // then + assertThat(result).isNull(); + } + + @Test + void givenAString_whenNullableIdEncodeIsCalled_thenReturnsEncodedString() { + // given + String input = "test"; + + // when + String result = nullableIdEncode(input); + + // then + assertThat(result).isEqualTo(idEncode(input)); + } + + @Test + void givenAnEncodedString_whenIdDecodeIsCalled_thenReturnsDecodedString() throws InvalidIdException { + // given + String input = "dGVzdC1zdHJpbmc"; + + // when + String result = idDecode(input); + + // then + assertThat(result).isEqualTo("test-string"); + } + + @Test + void givenAnInvalidEncodedString_whenIdDecodeIsCalled_thenThrowsInvalidIdException() { + // given + String input = "!!!not-base64!!!"; + + // when / then + assertThatThrownBy(() -> idDecode(input)) + .isInstanceOf(InvalidIdException.class); + } + + @Test + void givenANullString_whenNullableIdDecodeIsCalled_thenReturnsNull() throws InvalidIdException { + // given + String input = null; + + // when + String result = nullableIdDecode(input); + + // then + assertThat(result).isNull(); + } + + @Test + void givenAnEncodedString_whenNullableIdDecodeIsCalled_thenReturnsDecodedString() throws InvalidIdException { + // given + String input = "dGVzdA"; + + // when + String result = nullableIdDecode(input); + + // then + assertThat(result).isEqualTo("test"); + } +}