diff --git a/src/main/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiController.java b/src/main/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiController.java index 0f4408e..b615681 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiController.java +++ b/src/main/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiController.java @@ -10,6 +10,7 @@ import org.opendevstack.component_catalog.server.model.ProvisioningDeleteRequest; import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest; import org.opendevstack.component_catalog.server.services.ProvisionerActionsService; +import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponentRequest; import org.opendevstack.component_catalog.server.services.provisioner.Status; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; @@ -40,9 +41,15 @@ public ResponseEntity notifyProvisioningStatusUpdate(String projectKey, var normalizedComponentUrl = provisioningStatusUpdateRequest.getComponentUrl().orElse(Strings.EMPTY); var parameters = map(provisioningStatusUpdateRequest); - provisionerActionsService.updateComponentProvisioningStatus(normalizedProjectKey, Status.valueOf(status), - provisioningStatusUpdateRequest.getComponentId(), provisioningStatusUpdateRequest.getCatalogItemId(), - normalizedComponentUrl, parameters); + var request = ProjectComponentRequest.builder() + .componentId(provisioningStatusUpdateRequest.getComponentId()) + .catalogItemId(provisioningStatusUpdateRequest.getCatalogItemId()) + .status(Status.valueOf(status)) + .componentUrl(normalizedComponentUrl) + .parameters(parameters) + .build(); + + provisionerActionsService.updateComponentProvisioningStatus(normalizedProjectKey, request); return ResponseEntity.ok().build(); } @@ -58,9 +65,15 @@ public ResponseEntity notifyProvisioningStatusUpdatePartially(String proje var normalizedComponentUrl = provisioningStatusUpdateRequest.getComponentUrl().orElse(Strings.EMPTY); var parameters = map(provisioningStatusUpdateRequest); - provisionerActionsService.updatePartiallyComponentProvisioningStatus(normalizedProjectKey, Status.valueOf(status), - provisioningStatusUpdateRequest.getComponentId(), provisioningStatusUpdateRequest.getCatalogItemId(), - normalizedComponentUrl, provisioningStatusUpdateRequest.getWorkflowJobId().orElse(""), parameters); + var request = ProjectComponentRequest.builder() + .componentId(provisioningStatusUpdateRequest.getComponentId()) + .catalogItemId(provisioningStatusUpdateRequest.getCatalogItemId()) + .status(Status.valueOf(status)) + .componentUrl(normalizedComponentUrl) + .parameters(parameters) + .build(); + + provisionerActionsService.updatePartiallyComponentProvisioningStatus(normalizedProjectKey, request); return ResponseEntity.ok().build(); } diff --git a/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java b/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java index 7dcd8d6..ac90d86 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java +++ b/src/main/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacade.java @@ -1,6 +1,5 @@ package org.opendevstack.component_catalog.server.facade; -import jakarta.validation.constraints.NotNull; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.tuple.Pair; import org.jspecify.annotations.NonNull; @@ -10,6 +9,7 @@ import org.opendevstack.component_catalog.server.services.ProjectsInfoService; import org.opendevstack.component_catalog.server.services.catalog.CatalogItemUserActionGroupsRestriction; import org.opendevstack.component_catalog.server.services.catalog.common.UserActionEntityRestrictions; +import org.opendevstack.component_catalog.server.services.provisioner.Parameter; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.EvaluationRestrictions; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.GroupsRestrictionsEvaluator; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsParams; @@ -42,9 +42,9 @@ public ProvisionerActionsApiFacade(ProjectsInfoService projectsInfoService, } - public static @NonNull List>> map(ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) { + public static @NonNull List map(ProvisioningStatusUpdateRequest provisioningStatusUpdateRequest) { return provisioningStatusUpdateRequest.getParameters().stream() - .map(parameter -> Pair.of(parameter.getName(), parameter.getValues())) + .map(param -> Parameter.builder().name(param.getName()).values(param.getValues()).build()) .toList(); } diff --git a/src/main/java/org/opendevstack/component_catalog/server/services/ProjectComponentsService.java b/src/main/java/org/opendevstack/component_catalog/server/services/ProjectComponentsService.java index 0fd81cc..bdec146 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/services/ProjectComponentsService.java +++ b/src/main/java/org/opendevstack/component_catalog/server/services/ProjectComponentsService.java @@ -5,10 +5,7 @@ import org.apache.commons.lang3.StringUtils; import org.opendevstack.component_catalog.server.services.exceptions.InvalidComponentStateException; import org.opendevstack.component_catalog.server.services.exceptions.InvalidEntityException; -import org.opendevstack.component_catalog.server.services.provisioner.Parameter; -import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponent; -import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponents; -import org.opendevstack.component_catalog.server.services.provisioner.Status; +import org.opendevstack.component_catalog.server.services.provisioner.*; import org.springframework.stereotype.Service; import java.util.Base64; @@ -30,25 +27,23 @@ public ProjectComponents createNewComponent() { @SneakyThrows public ProjectComponents addNewComponent(ProjectComponents projectComponents, - String componentId, - String catalogItemId, - Status status, - String componentUrl, - List parameters) { - var catalogItemIdWithoutBranch = getRepoPathFromCatalogItemId(catalogItemId); - var branchReference = getBranchRefFromCatalogItemId(catalogItemId); + ProjectComponentRequest request) { + var catalogItemIdWithoutBranch = getRepoPathFromCatalogItemId(request.getCatalogItemId()); + var branchReference = getBranchRefFromCatalogItemId(request.getCatalogItemId()); var updatedComponents = Optional.ofNullable(projectComponents.getComponents()) .map(HashMap::new) .orElse(new HashMap<>()); - updatedComponents.put(componentId, ProjectComponent.builder() - .componentId(componentId) + updatedComponents.put(request.getComponentId(), ProjectComponent.builder() + .componentId(request.getComponentId()) .catalogItemId(catalogItemIdWithoutBranch) - .status(status) + .status(request.getStatus()) .catalogItemRef(branchReference) - .componentUrl(componentUrl) - .parameters(parameters) + .componentUrl(request.getComponentUrl()) + .createdAt(request.getCreatedAt()) + .updatedAt(request.getUpdatedAt()) + .parameters(request.getParameters()) .build()); var updatedProjectComponents = ProjectComponents.builder() @@ -62,21 +57,17 @@ public ProjectComponents addNewComponent(ProjectComponents projectComponents, @SneakyThrows public ProjectComponents updateExistingComponent(ProjectComponents projectComponents, - String componentId, - String catalogItemId, - Status status, - String componentUrl, - List parameters) { + ProjectComponentRequest request) { Map components = projectComponents.getComponents(); - if (!components.containsKey(componentId)) { - throw new InvalidComponentStateException("Component with id " + componentId + " does not exist"); + if (!components.containsKey(request.getComponentId())) { + throw new InvalidComponentStateException("Component with id " + request.getComponentId() + " does not exist"); } - var existing = components.get(componentId); - var catalogItemIdWithoutBranch = getRepoPathFromCatalogItemId(catalogItemId); - var branchReference = getBranchRefFromCatalogItemId(catalogItemId); + var existing = components.get(request.getComponentId()); + var catalogItemIdWithoutBranch = getRepoPathFromCatalogItemId(request.getCatalogItemId()); + var branchReference = getBranchRefFromCatalogItemId(request.getCatalogItemId()); if (!existing.getCatalogItemId().equals(catalogItemIdWithoutBranch)) { return projectComponents; @@ -85,14 +76,16 @@ public ProjectComponents updateExistingComponent(ProjectComponents projectCompon ProjectComponent updated = ProjectComponent.builder() .componentId(existing.getComponentId()) .catalogItemId(existing.getCatalogItemId()) - .status(status) + .status(request.getStatus()) .catalogItemRef(branchReference) - .componentUrl(StringUtils.isBlank(componentUrl) ? existing.getComponentUrl() : componentUrl) - .parameters(parameters) + .componentUrl(StringUtils.isBlank(request.getComponentUrl()) ? existing.getComponentUrl() : request.getComponentUrl()) + .createdAt(request.getCreatedAt()) + .updatedAt(request.getUpdatedAt()) + .parameters(request.getParameters()) .build(); Map updatedMap = new HashMap<>(components); - updatedMap.put(componentId, updated); + updatedMap.put(request.getComponentId(), updated); return ProjectComponents.builder() .components(updatedMap) @@ -101,28 +94,16 @@ public ProjectComponents updateExistingComponent(ProjectComponents projectCompon @SneakyThrows public ProjectComponents updatePartiallyExistingComponent(ProjectComponents projectComponents, - String componentId, - String catalogItemId, - Status status, - String componentUrl, - String workflowJobId, - List parameters) { + ProjectComponentRequest request) { - validateComponentExists(projectComponents, componentId); + validateComponentExists(projectComponents, request.getComponentId()); Map updatedMap = projectComponents.getComponents() .entrySet() .stream() .collect(Collectors.toMap( Map.Entry::getKey, - entry -> updateComponentIfMatch( - entry, - componentId, - catalogItemId, - status, - componentUrl, - workflowJobId, - parameters) + entry -> updateComponentIfMatch(entry, request) )); return ProjectComponents.builder() @@ -169,25 +150,22 @@ protected String getRepoPathFromCatalogItemId(String catalogItemId) throws Inval } private ProjectComponent updateComponentIfMatch(Map.Entry entry, - String componentId, - String catalogItemId, - Status status, - String componentUrl, - String workflowJobId, - List parameters) { - - if (!entry.getKey().equals(componentId)) { + ProjectComponentRequest request) { + + if (!entry.getKey().equals(request.getComponentId())) { return entry.getValue(); // leave unchanged } return ProjectComponent.builder() .componentId(entry.getValue().getComponentId()) .catalogItemId(entry.getValue().getCatalogItemId()) - .status(status) - .catalogItemRef(resolveCatalogItemRef(entry.getValue(), catalogItemId)) - .componentUrl(resolveComponentUrl(entry.getValue(), componentUrl)) - .workflowJobId(resolveWorkflowJobId(entry.getValue(), workflowJobId)) - .parameters(resolveParameters(entry.getValue(), parameters)) + .status(request.getStatus()) + .catalogItemRef(resolveCatalogItemRef(entry.getValue(), request.getCatalogItemId())) + .componentUrl(resolveComponentUrl(entry.getValue(), request.getComponentUrl())) + .workflowJobId(resolveWorkflowJobId(entry.getValue(), request.getWorkflowJobId())) + .createdAt(request.getCreatedAt()) + .updatedAt(request.getUpdatedAt()) + .parameters(resolveParameters(entry.getValue(), request.getParameters())) .build(); } diff --git a/src/main/java/org/opendevstack/component_catalog/server/services/ProvisionerActionsService.java b/src/main/java/org/opendevstack/component_catalog/server/services/ProvisionerActionsService.java index fbeb33b..6c82e04 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/services/ProvisionerActionsService.java +++ b/src/main/java/org/opendevstack/component_catalog/server/services/ProvisionerActionsService.java @@ -14,10 +14,7 @@ import org.opendevstack.component_catalog.server.services.exceptions.ComponentAlreadyExistsException; import org.opendevstack.component_catalog.server.services.exceptions.ElementNotFoundException; import org.opendevstack.component_catalog.server.services.exceptions.UnableToDeserializeEntityException; -import org.opendevstack.component_catalog.server.services.provisioner.Parameter; -import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponent; -import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponents; -import org.opendevstack.component_catalog.server.services.provisioner.Status; +import org.opendevstack.component_catalog.server.services.provisioner.*; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; @@ -37,68 +34,73 @@ public class ProvisionerActionsService { @Synchronized public void updateComponentProvisioningStatus(String projectKey, - Status status, - String componentId, - String catalogItemId, - String componentUrl, - List>> parameters) throws JsonProcessingException { //componentUrl can be null + ProjectComponentRequest request) throws JsonProcessingException { //componentUrl can be null log.debug("Processing provisioning status for projectKey: {}, status: {}, componentId: {}, catalogItemId: {}, componentUrl: {}", - projectKey, status, componentId, catalogItemId, componentUrl); + projectKey, request.getStatus(), request.getComponentId(), request.getCatalogItemId(), request.getComponentUrl()); var pathAt = getBitbucketPathAt(projectKey); - List projectComponentParameters = map(parameters); var sourceCommitId = bitbucketService.getLastCommit(pathAt).orElse(null); // If no sourceCommitId, that means is a new file var projectComponents = getProjectComponents(projectKey); - validate(projectComponents, componentId, status); + validate(projectComponents, request.getComponentId(), request.getStatus()); - var existsComponent = componentExistsInProjectComponents(projectComponents, componentId); + var existsComponent = componentExistsInProjectComponents(projectComponents, request.getComponentId()); ProjectComponents updatedProjectComponents; + var currentTimestamp = String.valueOf(System.currentTimeMillis()); + + request.setUpdatedAt(currentTimestamp); + if (existsComponent) { - log.trace("Updating componentKey: {} to projectComponents: {}. Status: {}", componentId, projectComponents, status); + log.trace("Updating componentKey: {} to projectComponents: {}. Status: {}", request.getComponentId(), projectComponents, request.getStatus()); + + var createdAt = projectComponents.getComponents().get(request.getComponentId()).getCreatedAt(); + request.setCreatedAt(createdAt); updatedProjectComponents = projectComponentsService.updateExistingComponent( - projectComponents, componentId, catalogItemId, status, componentUrl, projectComponentParameters); + projectComponents, request); } else { - log.trace("Adding new componentKey: {} to projectComponents: {}", componentId, projectComponents); + log.trace("Adding new componentKey: {} to projectComponents: {}", request.getComponentId(), projectComponents); + + request.setCreatedAt(currentTimestamp); updatedProjectComponents = projectComponentsService.addNewComponent( - projectComponents, componentId, catalogItemId, status, componentUrl, projectComponentParameters); + projectComponents, request); } // Update file with new status saveProjectComponents(pathAt, sourceCommitId, updatedProjectComponents); + log.trace("{} component with timestamp {}", (existsComponent ? "Updated" : "Created"), currentTimestamp); } @Synchronized public void updatePartiallyComponentProvisioningStatus(String projectKey, - Status status, - String componentId, - String catalogItemId, - String componentUrl, - String workflowJobId, - List>> parameters) throws JsonProcessingException { //componentUrl can be null + ProjectComponentRequest request) throws JsonProcessingException { //componentUrl can be null log.debug("Processing provisioning status for projectKey: {}, status: {}, componentId: {}, catalogItemId: {}, componentUrl: {}", - projectKey, status, componentId, catalogItemId, componentUrl); + projectKey, request.getStatus(), request.getComponentId(), request.getCatalogItemId(), request.getComponentUrl()); var pathAt = getBitbucketPathAt(projectKey); - List projectComponentParameters = map(parameters); var sourceCommitId = bitbucketService.getLastCommit(pathAt).orElse(null); // If no sourceCommitId, that means is a new file var projectComponents = getProjectComponents(projectKey); - if (projectComponents == null || projectComponents.getComponents() == null) { + if (projectComponents == null || projectComponents.getComponents() == null || !projectComponents.getComponents().containsKey(request.getComponentId())) { throw new ElementNotFoundException("In a partial update, the projectComponent should exist."); } - log.trace("Updating partially componentKey: {} to projectComponents: {}. Status: {}", componentId, projectComponents, status); + var currentTimestamp = System.currentTimeMillis(); + request.setUpdatedAt(projectComponents.getComponents().get(request.getComponentId()).getUpdatedAt()); + projectComponents.getComponents().get(request.getComponentId()).setUpdatedAt(String.valueOf(currentTimestamp)); + + log.trace("Updating partially componentKey: {} to projectComponents: {}. Status: {}", request.getComponentId(), projectComponents, request.getStatus()); + var updatedProjectComponents = projectComponentsService.updatePartiallyExistingComponent( - projectComponents, componentId, catalogItemId, status, componentUrl, workflowJobId, projectComponentParameters); + projectComponents, request); // Update file with new status saveProjectComponents(pathAt, sourceCommitId, updatedProjectComponents); + log.trace("Updated component with timestamp {}", currentTimestamp); } @Synchronized @@ -160,12 +162,6 @@ protected void saveProjectComponents(BitbucketPathAt pathAt, String sourceCommit } } - private static @NonNull List map(List>> parameters) { - return parameters.stream() - .map(pair -> Parameter.builder().name(pair.getLeft()).values(pair.getRight()).build()) - .toList(); - } - private void validateComponentDoesNotExistsWhenCreating(ProjectComponents projectComponents, String componentId) { if (componentExistsInProjectComponents(projectComponents, componentId)) { throw new ComponentAlreadyExistsException("Component with id '" + componentId + "' already exists in the project components."); diff --git a/src/main/java/org/opendevstack/component_catalog/server/services/provisioner/ProjectComponent.java b/src/main/java/org/opendevstack/component_catalog/server/services/provisioner/ProjectComponent.java index 65514d2..abdac60 100644 --- a/src/main/java/org/opendevstack/component_catalog/server/services/provisioner/ProjectComponent.java +++ b/src/main/java/org/opendevstack/component_catalog/server/services/provisioner/ProjectComponent.java @@ -22,6 +22,8 @@ public class ProjectComponent { private Status status; private String componentUrl; private String workflowJobId; + private String createdAt; + private String updatedAt; private List parameters; @Value("${catalog-item.reference.encoded}") diff --git a/src/main/java/org/opendevstack/component_catalog/server/services/provisioner/ProjectComponentRequest.java b/src/main/java/org/opendevstack/component_catalog/server/services/provisioner/ProjectComponentRequest.java new file mode 100644 index 0000000..07d97e8 --- /dev/null +++ b/src/main/java/org/opendevstack/component_catalog/server/services/provisioner/ProjectComponentRequest.java @@ -0,0 +1,19 @@ +package org.opendevstack.component_catalog.server.services.provisioner; + +import lombok.Builder; +import lombok.Data; + +import java.util.List; + +@Builder +@Data +public class ProjectComponentRequest { + private String componentId; + private String catalogItemId; + private Status status; + private String componentUrl; + private String workflowJobId; + private String createdAt; + private String updatedAt; + private List parameters; +} diff --git a/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java b/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java index d70075c..3e60492 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/controllers/ProvisionerActionsApiControllerTest.java @@ -1,7 +1,6 @@ package org.opendevstack.component_catalog.server.controllers; import com.fasterxml.jackson.core.JsonProcessingException; -import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -11,6 +10,8 @@ import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest; import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequestParametersInner; import org.opendevstack.component_catalog.server.services.ProvisionerActionsService; +import org.opendevstack.component_catalog.server.services.provisioner.Parameter; +import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponentRequest; import org.opendevstack.component_catalog.server.services.provisioner.Status; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -31,6 +32,24 @@ class ProvisionerActionsApiControllerTest { @InjectMocks private ProvisionerActionsApiController provisionerActionsApiController; + // helper + private ProjectComponentRequest request(String componentId, + String catalogItemId, + Status status, + String url, + List params) { + return ProjectComponentRequest.builder() + .componentId(componentId) + .catalogItemId(catalogItemId) + .status(status) + .componentUrl(url) + .workflowJobId(null) + .createdAt(null) + .updatedAt(null) + .parameters(params) + .build(); + } + @Test void givenAProjectKey_whenNotifyProvisioningCompleted_thenServiceIsCalled() throws JsonProcessingException { // given @@ -39,7 +58,12 @@ void givenAProjectKey_whenNotifyProvisioningCompleted_thenServiceIsCalled() thro var componentId = "componentId"; var catalogItemId = "catalogItemId"; var componentUrl = "componentUrl"; - var parameter = ProvisioningStatusUpdateRequestParametersInner.builder() + var parameterInner = ProvisioningStatusUpdateRequestParametersInner.builder() + .name("parameterName") + .values(List.of("parameterValue")) + .build(); + var parametersInner = List.of(parameterInner); + var parameter = Parameter.builder() .name("parameterName") .values(List.of("parameterValue")) .build(); @@ -49,9 +73,7 @@ void givenAProjectKey_whenNotifyProvisioningCompleted_thenServiceIsCalled() thro .componentId(componentId) .catalogItemId(catalogItemId) .componentUrl(componentUrl) - .parameters(parameters); - - var mappedParameters = List.of(Pair.of(parameter.getName(), parameter.getValues())); + .parameters(parametersInner); // when provisionerActionsApiController.notifyProvisioningStatusUpdate(projectKey, status.name(), request); @@ -59,7 +81,8 @@ void givenAProjectKey_whenNotifyProvisioningCompleted_thenServiceIsCalled() thro // then verify(provisionerActionsApiFacade).validateGroupRestrictions(eq(projectKey.toUpperCase())); verify(provisionerActionsService).updateComponentProvisioningStatus(projectKey.toUpperCase(), - status, componentId, catalogItemId, componentUrl, mappedParameters); + request(componentId, catalogItemId, status, componentUrl, parameters) + ); } @Test @@ -70,21 +93,22 @@ void givenAProjectKey_whenNotifyProvisioningStatusUpdatePartially_thenServiceIsC var componentId = "componentId"; var catalogItemId = "catalogItemId"; var componentUrl = "componentUrl"; - var parameter = ProvisioningStatusUpdateRequestParametersInner.builder() + var parameterInner = ProvisioningStatusUpdateRequestParametersInner.builder() + .name("parameterName") + .values(List.of("parameterValue")) + .build(); + var parametersInner = List.of(parameterInner); + var parameter = Parameter.builder() .name("parameterName") .values(List.of("parameterValue")) .build(); - var workflowJobId = "workflowJobId"; var parameters = List.of(parameter); var request = new ProvisioningStatusUpdateRequest() .componentId(componentId) .catalogItemId(catalogItemId) .componentUrl(componentUrl) - .workflowJobId(workflowJobId) - .parameters(parameters); - - var mappedParameters = List.of(Pair.of(parameter.getName(), parameter.getValues())); + .parameters(parametersInner); // when provisionerActionsApiController.notifyProvisioningStatusUpdatePartially(projectKey, status.name(), request); @@ -93,12 +117,7 @@ void givenAProjectKey_whenNotifyProvisioningStatusUpdatePartially_thenServiceIsC verify(provisionerActionsApiFacade).validateGroupRestrictions(eq(projectKey.toUpperCase())); verify(provisionerActionsService).updatePartiallyComponentProvisioningStatus( projectKey.toUpperCase(), - status, - componentId, - catalogItemId, - componentUrl, - workflowJobId, - mappedParameters + request(componentId, catalogItemId, status, componentUrl, parameters) ); } @@ -109,8 +128,12 @@ void givenAProjectKeyAndNoComponentUrl_whenNotifyProvisioningStatusUpdatePartial var status = Status.CREATING; var componentId = "componentId"; var catalogItemId = "catalogItemId"; - var workflowJobId = "workflowJobId"; - var parameter = ProvisioningStatusUpdateRequestParametersInner.builder() + var parameterInner = ProvisioningStatusUpdateRequestParametersInner.builder() + .name("parameterName") + .values(List.of("parameterValue")) + .build(); + var parametersInner = List.of(parameterInner); + var parameter = Parameter.builder() .name("parameterName") .values(List.of("parameterValue")) .build(); @@ -119,10 +142,7 @@ void givenAProjectKeyAndNoComponentUrl_whenNotifyProvisioningStatusUpdatePartial var request = new ProvisioningStatusUpdateRequest() .componentId(componentId) .catalogItemId(catalogItemId) - .workflowJobId(workflowJobId) - .parameters(parameters); - - var mappedParameters = List.of(Pair.of(parameter.getName(), parameter.getValues())); + .parameters(parametersInner); // when provisionerActionsApiController.notifyProvisioningStatusUpdatePartially(projectKey, status.name(), request); @@ -130,17 +150,23 @@ void givenAProjectKeyAndNoComponentUrl_whenNotifyProvisioningStatusUpdatePartial // then verify(provisionerActionsApiFacade).validateGroupRestrictions(eq(projectKey.toUpperCase())); verify(provisionerActionsService).updatePartiallyComponentProvisioningStatus(projectKey.toUpperCase(), - status, componentId, catalogItemId, "", workflowJobId, mappedParameters); + request(componentId, catalogItemId, status, "", parameters) + ); } @Test void givenAProjectKeyAndNoComponentUrl_whenNotifyProvisioningStatusUpdate_thenServiceIsCalledWithEmptyUrl() throws JsonProcessingException { // given var projectKey = "projectKey"; - var status = Status.CREATED; + var status = Status.CREATING; var componentId = "componentId"; var catalogItemId = "catalogItemId"; - var parameter = ProvisioningStatusUpdateRequestParametersInner.builder() + var parameterInner = ProvisioningStatusUpdateRequestParametersInner.builder() + .name("parameterName") + .values(List.of("parameterValue")) + .build(); + var parametersInner = List.of(parameterInner); + var parameter = Parameter.builder() .name("parameterName") .values(List.of("parameterValue")) .build(); @@ -149,9 +175,7 @@ void givenAProjectKeyAndNoComponentUrl_whenNotifyProvisioningStatusUpdate_thenSe var request = new ProvisioningStatusUpdateRequest() .componentId(componentId) .catalogItemId(catalogItemId) - .parameters(parameters); - - var mappedParameters = List.of(Pair.of(parameter.getName(), parameter.getValues())); + .parameters(parametersInner); // when provisionerActionsApiController.notifyProvisioningStatusUpdate(projectKey, status.name(), request); @@ -159,7 +183,8 @@ void givenAProjectKeyAndNoComponentUrl_whenNotifyProvisioningStatusUpdate_thenSe // then verify(provisionerActionsApiFacade).validateGroupRestrictions(eq(projectKey.toUpperCase())); verify(provisionerActionsService).updateComponentProvisioningStatus(projectKey.toUpperCase(), - status, componentId, catalogItemId, "", mappedParameters); + request(componentId, catalogItemId, status, "", parameters) + ); } @Test diff --git a/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java b/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java index b28467e..4f259a3 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/facade/ProvisionerActionsApiFacadeTest.java @@ -11,6 +11,7 @@ import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequest; import org.opendevstack.component_catalog.server.model.ProvisioningStatusUpdateRequestParametersInner; import org.opendevstack.component_catalog.server.services.ProjectsInfoService; +import org.opendevstack.component_catalog.server.services.provisioner.Parameter; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.EvaluationRestrictions; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.GroupsRestrictionsEvaluator; import org.opendevstack.component_catalog.server.services.restrictions.evaluators.RestrictionsParams; @@ -50,7 +51,7 @@ void setUp() { } @Test - void map_convertsParametersToPairs() { + void map_convertsParametersToPlainParameter() { // given var parameter1 = ProvisioningStatusUpdateRequestParametersInner.builder() .name("param1") @@ -68,8 +69,14 @@ void map_convertsParametersToPairs() { // then assertThat(result).containsExactly( - Pair.of("param1", List.of("value1", "value2")), - Pair.of("param2", List.of("value3")) + Parameter.builder() + .name("param1") + .values(List.of("value1", "value2")) + .build(), + Parameter.builder() + .name("param2") + .values(List.of("value3")) + .build() ); } diff --git a/src/test/java/org/opendevstack/component_catalog/server/services/ProjectComponentsServiceTest.java b/src/test/java/org/opendevstack/component_catalog/server/services/ProjectComponentsServiceTest.java index 266113b..4ffae20 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/services/ProjectComponentsServiceTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/services/ProjectComponentsServiceTest.java @@ -1,17 +1,11 @@ package org.opendevstack.component_catalog.server.services; -import org.opendevstack.component_catalog.server.services.exceptions.InvalidComponentStateException; -import org.opendevstack.component_catalog.server.services.provisioner.Parameter; -import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponent; -import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponents; -import org.opendevstack.component_catalog.server.services.provisioner.Status; import org.junit.jupiter.api.Test; +import org.opendevstack.component_catalog.server.services.exceptions.InvalidComponentStateException; +import org.opendevstack.component_catalog.server.services.provisioner.*; import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -21,7 +15,28 @@ class ProjectComponentsServiceTest { private final ProjectComponentsService service = new ProjectComponentsService(); private String base64(String val) { - return java.util.Base64.getUrlEncoder().encodeToString(val.getBytes(StandardCharsets.UTF_8)); + return Base64.getUrlEncoder().encodeToString(val.getBytes(StandardCharsets.UTF_8)); + } + + // helper + private ProjectComponentRequest request(String componentId, + String catalogItemId, + Status status, + String url, + String workflowJobId, + String createdAt, + String updatedAt, + List params) { + return ProjectComponentRequest.builder() + .componentId(componentId) + .catalogItemId(catalogItemId) + .status(status) + .componentUrl(url) + .workflowJobId(workflowJobId) + .createdAt(createdAt) + .updatedAt(updatedAt) + .parameters(params) + .build(); } @Test @@ -43,7 +58,10 @@ void givenValidInput_whenAddNewComponent_thenComponentAdded() { String encoded = base64("repo/path?at=refs/heads/main"); //when - ProjectComponents updated = service.addNewComponent(pc, "comp1", encoded, Status.CREATING, "url", Collections.emptyList()); + ProjectComponents updated = service.addNewComponent( + pc, + request("comp1", encoded, Status.CREATING, "url", null, "created", "updated", Collections.emptyList()) + ); //then assertThat(updated.getComponents()).containsKey("comp1"); @@ -54,6 +72,8 @@ void givenValidInput_whenAddNewComponent_thenComponentAdded() { assertThat(added.getCatalogItemRef()).isEqualTo(base64("?at=refs/heads/main")); assertThat(added.getStatus()).isEqualTo(Status.CREATING); assertThat(added.getComponentUrl()).isEqualTo("url"); + assertThat(added.getCreatedAt()).isEqualTo("created"); + assertThat(added.getUpdatedAt()).isEqualTo("updated"); } @Test @@ -70,6 +90,8 @@ void givenExistingComponent_whenUpdateExistingComponent_thenUpdatedCorrectly() { .catalogItemRef(null) .componentUrl("oldUrl") .status(Status.CREATING) + .createdAt("oldCreated") + .updatedAt("oldUpdated") .build(); ProjectComponents pc = ProjectComponents.builder() @@ -77,8 +99,10 @@ void givenExistingComponent_whenUpdateExistingComponent_thenUpdatedCorrectly() { .build(); //when - ProjectComponents updated = - service.updateExistingComponent(pc, "comp1", encodedFull, Status.CREATED, "newUrl", parameters); + ProjectComponents updated = service.updateExistingComponent( + pc, + request("comp1", encodedFull, Status.CREATED, "newUrl", null, "created", "updated", parameters) + ); //then ProjectComponent updatedComp = updated.getComponents().get("comp1"); @@ -87,6 +111,8 @@ void givenExistingComponent_whenUpdateExistingComponent_thenUpdatedCorrectly() { assertThat(updatedComp.getCatalogItemRef()).isEqualTo(base64("?at=refs/heads/main")); assertThat(updatedComp.getComponentUrl()).isEqualTo("newUrl"); assertThat(updatedComp.getParameters()).containsExactly(parameter); + assertThat(updatedComp.getCreatedAt()).isEqualTo("created"); + assertThat(updatedComp.getUpdatedAt()).isEqualTo("updated"); } @Test @@ -105,8 +131,10 @@ void givenDifferentRepoPath_whenUpdateExistingComponent_thenDoNotUpdateComponent .build(); //when - ProjectComponents updated = - service.updateExistingComponent(pc, "comp1", encodedFullDifferent, Status.CREATED, "x", Collections.emptyList()); + ProjectComponents updated = service.updateExistingComponent( + pc, + request("comp1", encodedFullDifferent, Status.CREATED, "x", null, "created", "updated", Collections.emptyList()) + ); //then assertThat(updated.getComponents().get("comp1").getCatalogItemId()) @@ -122,7 +150,10 @@ void givenNonExistingComponent_whenUpdateExistingComponent_thenThrow() { //when //then assertThatThrownBy(() -> - service.updateExistingComponent(pc, "unknown", "zzz", Status.CREATED, "x", Collections.emptyList())) + service.updateExistingComponent( + pc, + request("unknown", "zzz", Status.CREATED, "x", null, "created", "updated", Collections.emptyList()) + )) .isInstanceOf(InvalidComponentStateException.class); } @@ -141,6 +172,8 @@ void givenExistingComponent_whenUpdatePartially_thenUpdatesOnlyFieldsProvided() .catalogItemRef(base64("?at=refs/heads/main")) .componentUrl("oldUrl") .status(Status.CREATING) + .createdAt("oldCreatedAt") + .updatedAt("oldUpdatedAt") .build(); ProjectComponents pc = ProjectComponents.builder() @@ -148,8 +181,10 @@ void givenExistingComponent_whenUpdatePartially_thenUpdatesOnlyFieldsProvided() .build(); //when - ProjectComponents updated = - service.updatePartiallyExistingComponent(pc, "comp1", encodedFull, Status.CREATED, null, null, parameters); + ProjectComponents updated = service.updatePartiallyExistingComponent( + pc, + request("comp1", encodedFull, Status.CREATED, null, null, "created", "updated", parameters) + ); //then ProjectComponent result = updated.getComponents().get("comp1"); @@ -157,6 +192,8 @@ void givenExistingComponent_whenUpdatePartially_thenUpdatesOnlyFieldsProvided() assertThat(result.getStatus()).isEqualTo(Status.CREATED); assertThat(result.getComponentUrl()).isEqualTo("oldUrl"); // unchanged assertThat(result.getCatalogItemRef()).isEqualTo(base64("?at=refs/heads/dev")); + assertThat(result.getCreatedAt()).isEqualTo("created"); + assertThat(result.getUpdatedAt()).isEqualTo("updated"); } @Test @@ -168,7 +205,10 @@ void givenNonExistingComponent_whenUpdatePartially_thenThrow() { //when //then assertThatThrownBy(() -> - service.updatePartiallyExistingComponent(pc, "missing", "zzz", Status.CREATED, "x", null, Collections.emptyList())) + service.updatePartiallyExistingComponent( + pc, + request("missing", "zzz", Status.CREATED, "x", null, "created", "updated", Collections.emptyList()) + )) .isInstanceOf(InvalidComponentStateException.class); } @@ -203,8 +243,10 @@ void givenBlankWorkflowJobId_whenUpdatePartially_thenKeepsExistingWorkflowJobId( .build(); //when - ProjectComponents updated = - service.updatePartiallyExistingComponent(pc, "comp1", null, Status.CREATED, null, "", Collections.emptyList()); + ProjectComponents updated = service.updatePartiallyExistingComponent( + pc, + request("comp1", null, Status.CREATED, null, "", null, null, Collections.emptyList()) + ); //then assertThat(updated.getComponents().get("comp1").getWorkflowJobId()).isEqualTo("existing-job-id"); @@ -226,8 +268,10 @@ void givenNewWorkflowJobId_whenUpdatePartially_thenUsesNewWorkflowJobId() { .build(); //when - ProjectComponents updated = - service.updatePartiallyExistingComponent(pc, "comp1", null, Status.CREATED, null, "new-job-id", Collections.emptyList()); + ProjectComponents updated = service.updatePartiallyExistingComponent( + pc, + request("comp1", null, Status.CREATED, null, "new-job-id", null, null, Collections.emptyList()) + ); //then assertThat(updated.getComponents().get("comp1").getWorkflowJobId()).isEqualTo("new-job-id"); @@ -257,4 +301,34 @@ void givenValidCatalogItemId_whenExtractBranchRef_thenReturnEncodedBranch() { assertThat(repoPath).isEqualTo(base64("repo/x")); } + @Test + void givenNullTimestamps_whenUpdatePartially_thenTimestampsAreOverwrittenWithNull() { + //given + String encodedRepo = base64("repo/a"); + + ProjectComponent existing = ProjectComponent.builder() + .componentId("comp1") + .catalogItemId(encodedRepo) + .createdAt("oldCreated") + .updatedAt("oldUpdated") + .status(Status.CREATING) + .build(); + + ProjectComponents pc = ProjectComponents.builder() + .components(Map.of("comp1", existing)) + .build(); + + //when + ProjectComponents updated = service.updatePartiallyExistingComponent( + pc, + request("comp1", null, Status.CREATED, null, null, null, null, Collections.emptyList()) + ); + + //then + ProjectComponent result = updated.getComponents().get("comp1"); + + assertThat(result.getCreatedAt()).isNull(); + assertThat(result.getUpdatedAt()).isNull(); + } + } \ No newline at end of file diff --git a/src/test/java/org/opendevstack/component_catalog/server/services/ProvisionerActionsServiceTest.java b/src/test/java/org/opendevstack/component_catalog/server/services/ProvisionerActionsServiceTest.java index b498533..56e3618 100644 --- a/src/test/java/org/opendevstack/component_catalog/server/services/ProvisionerActionsServiceTest.java +++ b/src/test/java/org/opendevstack/component_catalog/server/services/ProvisionerActionsServiceTest.java @@ -1,553 +1,656 @@ -package org.opendevstack.component_catalog.server.services; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; -import org.apache.commons.lang3.tuple.Pair; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.opendevstack.component_catalog.config.ProvisionerActionsConfiguration; -import org.opendevstack.component_catalog.server.controllers.exceptions.RestEntityNotFoundException; -import org.opendevstack.component_catalog.server.mother.BitbucketPathAtMother; -import org.opendevstack.component_catalog.server.mother.ProjectComponentsMother; -import org.opendevstack.component_catalog.server.services.bitbucket.BitbucketPathAt; -import org.opendevstack.component_catalog.server.services.exceptions.ComponentAlreadyExistsException; -import org.opendevstack.component_catalog.server.services.exceptions.ElementNotFoundException; -import org.opendevstack.component_catalog.server.services.exceptions.InvalidEntityException; -import org.opendevstack.component_catalog.server.services.provisioner.Parameter; -import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponent; -import org.opendevstack.component_catalog.server.services.provisioner.ProjectComponents; -import org.opendevstack.component_catalog.server.services.provisioner.Status; -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.web.client.HttpClientErrorException; - -import java.util.Base64; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Optional; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; - -@ExtendWith(MockitoExtension.class) -class ProvisionerActionsServiceTest { - - @Mock - private BitbucketService bitbucketService; - - @Mock - private ProjectComponentsService projectComponentsService; - - @Mock - private ObjectMapper objectMapper; - - @Mock - private ObjectWriter objectWriter; - - @Mock - private BitbucketPathAt.BitbucketPathAtBuilder bitbucketPathAtBuilder; - - private ProvisionerActionsConfiguration provisionerActionsConfiguration; - - private ProvisionerActionsService provisionerActionsService; - - @BeforeEach - void setUp() { - provisionerActionsConfiguration = new ProvisionerActionsConfiguration(); - populateProvisionerActionsConfiguration(); - - provisionerActionsService = new ProvisionerActionsService(bitbucketService, objectMapper, projectComponentsService, provisionerActionsConfiguration); - } - - @Test - void givenAProvisionersObject_andCreatingStatus_whenValidate_andComponentIdExists_thenThrowException() { - // given - var componentId = "componentId"; - var status = Status.CREATING; - - ProjectComponents projectComponents = ProjectComponentsMother.of(); - - - // when - var exception = assertThrows(ComponentAlreadyExistsException.class, () -> - provisionerActionsService.validate(projectComponents, componentId, status) - ); - - // then - assertThat(exception.getMessage()).isEqualTo("Component with id 'componentId' already exists in the project components."); - } - - @Test - void givenAProvisionersObject_andCreatingStatus_whenUpdateComponentProvisioningStatus_thenBitbucketFileIsUpdated() throws JsonProcessingException { - // given - var projectKey = "projectKey"; - var status = Status.CREATING; - var componentId = "componentId"; - var catalogItemId = "catalogItemId"; - var componentUrl = "catalogUrl"; - - var pathAt = BitbucketPathAtMother.of(); - var sourceCommitId = "sourceCommitId"; - - var projectComponents = new ProjectComponents(); - var updatedProjectComponents = ProjectComponentsMother.of(); - - var parameterParam = Pair.of("parameterName", List.of("parameterValue")); - - var parameter = Parameter.builder().name(parameterParam.getLeft()).values(parameterParam.getValue()).build(); - var parameters = List.of(parameter); - - prepareMocksForGetBitbucketPathAt(pathAt); - when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.of(sourceCommitId)); - - prepareMocksForGetNonExistingProjectComponents(pathAt, projectComponents); - when(projectComponentsService.addNewComponent( - projectComponents, - componentId, - catalogItemId, - status, - componentUrl, - parameters - )).thenReturn(updatedProjectComponents); - - var serializedUpdatedProjectComponents = prepareMocksForSave(updatedProjectComponents); - - // when - provisionerActionsService.updateComponentProvisioningStatus( - projectKey, - status, - componentId, - catalogItemId, - componentUrl, - List.of(parameterParam) - ); - - // then - verify(bitbucketService).pushFile( - pathAt, - sourceCommitId, - serializedUpdatedProjectComponents - ); - } - - @ParameterizedTest - @EnumSource(value = Status.class, names = { "CREATED", "FAILED", "DELETING", "UNKNOWN" }) - void givenAProvisionersObject_andSelectedStatuses_whenUpdateComponentProvisioningStatus_thenBitbucketFileIsUpdated( - Status status - ) throws JsonProcessingException { - // given - var projectKey = "projectKey"; - var componentId = "componentId"; - var catalogItemId = "catalogItemId"; - var componentUrl = "catalogUrl"; - - var pathAt = BitbucketPathAtMother.of(); - var sourceCommitId = "sourceCommitId"; - - var projectComponents = ProjectComponentsMother.of(); - var updatedProjectComponents = ProjectComponentsMother.of(); - - prepareMocksForGetBitbucketPathAt(pathAt); - when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.of(sourceCommitId)); - - prepareMocksForGetExistingProjectComponents(pathAt, projectComponents); - when(projectComponentsService.updateExistingComponent( - projectComponents, - componentId, - catalogItemId, - status, - componentUrl, - Collections.emptyList() - )).thenReturn(updatedProjectComponents); - - var serializedUpdatedProjectComponents = prepareMocksForSave(updatedProjectComponents); - - // when - provisionerActionsService.updateComponentProvisioningStatus( - projectKey, - status, - componentId, - catalogItemId, - componentUrl, - Collections.emptyList() - ); - - // then - verify(bitbucketService).pushFile( - pathAt, - sourceCommitId, - serializedUpdatedProjectComponents - ); - } - - @Test - void givenASetOfProjectComponents_andACatalogItemId_andCatalogItemIdInComponents_whenIsProvisioned_thenReturnTrue() throws InvalidEntityException { - // given - var rawId = "my-item"; - var rawIdWithBranch = "my-item?at=refs/heads/main"; - - var encodedId = Base64.getEncoder().encodeToString(rawId.getBytes()); - var encodedIdWithBranch = Base64.getEncoder().encodeToString(rawIdWithBranch.getBytes()); - - var matchingComponent = new ProjectComponent(); - matchingComponent.setCatalogItemId(encodedId); - - var dummy1 = new ProjectComponent(); - dummy1.setCatalogItemId(Base64.getEncoder().encodeToString("dummy-1".getBytes())); - - var dummy2 = new ProjectComponent(); - dummy2.setCatalogItemId(Base64.getEncoder().encodeToString("dummy-2".getBytes())); - - var components = new HashMap(); - components.put("c1", dummy1); - components.put("c2", matchingComponent); // only this one matches - components.put("c3", dummy2); - - var projectComponents = new ProjectComponents(); - projectComponents.setComponents(components); - - when(projectComponentsService.getRepoPathFromCatalogItemId(anyString())).thenCallRealMethod(); - - // when - var result = provisionerActionsService.isProvisioned(projectComponents, encodedIdWithBranch); - - // then - assertThat(result).isTrue(); - } - - @Test - void givenASetOfProjectComponents_andACatalogItemId_andCatalogItemIdNotInComponents_whenIsProvisioned_thenReturnFalse() throws InvalidEntityException { - // given - var rawId = "my-item"; - var encodedId = Base64.getEncoder().encodeToString(rawId.getBytes()); - - var dummy1 = new ProjectComponent(); - dummy1.setCatalogItemId(Base64.getEncoder().encodeToString("dummy-1".getBytes())); - - var dummy2 = new ProjectComponent(); - dummy2.setCatalogItemId(Base64.getEncoder().encodeToString("dummy-2".getBytes())); - - var components = new HashMap(); - components.put("c1", dummy1); - components.put("c2", dummy2); - - var projectComponents = new ProjectComponents(); - projectComponents.setComponents(components); - - when(projectComponentsService.getRepoPathFromCatalogItemId(anyString())).thenCallRealMethod(); - - // when - var result = provisionerActionsService.isProvisioned(projectComponents, encodedId); - - // then - assertThat(result).isFalse(); - } - - @Test - void givenAProjectKey_andAComponentId_whenDeleteComponentProvisioningStatus_thenBitbucketServiceIsCalled() throws JsonProcessingException { - // given - var projectKey = "projectKey"; - var componentId = "componentId"; - - var pathAtBuilder = mock(BitbucketPathAt.BitbucketPathAtBuilder.class); - var pathAt = mock(BitbucketPathAt.class); - var sourceCommitId = "sourceCommitId"; - - var projectComponents = ProjectComponentsMother.of(); - var projectComponentsWithoutComponentId = ProjectComponentsMother.of(); - - when(bitbucketService.pathAtBuilder()).thenReturn(pathAtBuilder); - when(pathAtBuilder.projectKey(any())).thenReturn(pathAtBuilder); - when(pathAtBuilder.repoSlug(any())).thenReturn(pathAtBuilder); - when(pathAtBuilder.subPath(any())).thenReturn(pathAtBuilder); - when(pathAtBuilder.at(any())).thenReturn(pathAtBuilder); - when(pathAtBuilder.build()).thenReturn(pathAt); - - when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.of(sourceCommitId)); - when(projectComponentsService.deleteComponent(projectComponents, componentId)).thenReturn(projectComponentsWithoutComponentId); - - prepareMocksForGetExistingProjectComponents(pathAt, projectComponents); - var serializedProjectComponentsWithoutComponentId = prepareMocksForSave(projectComponentsWithoutComponentId); - - // when - provisionerActionsService.deleteComponentProvisioningStatus(projectKey, componentId); - - // then - verify(bitbucketService).pushFile(pathAt, sourceCommitId, serializedProjectComponentsWithoutComponentId); - } - - @Test - void givenAProjectKey_andAComponentId_whenDeleteComponentProvisioningStatus_andNoProjectComponentsForProjectKey_thenExceptionIsThrown() { - // given - var projectKey = "projectKey"; - var componentId = "componentId"; - - var pathAtBuilder = mock(BitbucketPathAt.BitbucketPathAtBuilder.class); - var pathAt = mock(BitbucketPathAt.class); - - var projectComponents = ProjectComponentsMother.of(); - - when(bitbucketService.pathAtBuilder()).thenReturn(pathAtBuilder); - when(pathAtBuilder.projectKey(any())).thenReturn(pathAtBuilder); - when(pathAtBuilder.repoSlug(any())).thenReturn(pathAtBuilder); - when(pathAtBuilder.subPath(any())).thenReturn(pathAtBuilder); - when(pathAtBuilder.at(any())).thenReturn(pathAtBuilder); - when(pathAtBuilder.build()).thenReturn(pathAt); - - when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.empty()); - - assertThat(projectComponents.getComponents()).containsKey(componentId); - - // when - var exception = assertThrows(RestEntityNotFoundException.class, () -> - provisionerActionsService.deleteComponentProvisioningStatus(projectKey, componentId) - ); - - // then - assertThat(exception.getMessage()).startsWith("No component provisioning status for pathAt:"); - verify(bitbucketService, times(0)).pushFile(any(), any(), anyString()); - } - - @Test - void givenAProjectComponents_whenSaveProjectComponents_andBitbucketRejectAsNoUpdates_ThenExceptionIsIgnored() throws JsonProcessingException { - // given - var pathAt = mock(BitbucketPathAt.class); - var sourceCommitId = "sourceCommitId"; - var updatedProjectComponents = ProjectComponentsMother.of(); - var httpClientErrorException = new HttpClientErrorException(HttpStatus.CONFLICT, "\"{\"errors\":" + - "[{\"context\":null,\"message\":\"The content provided is the same as what already exists. No change was committed.\"" + - ",\"exceptionName\":\"com.atlassian.bitbucket.content.FileContentUnmodifiedException\"}]}\""); - - doThrow(httpClientErrorException).when(bitbucketService).pushFile(eq(pathAt), eq(sourceCommitId), anyString()); - prepareMocksForSave(updatedProjectComponents); - - // when - provisionerActionsService.saveProjectComponents(pathAt, sourceCommitId, updatedProjectComponents); - - // then - // no exception is thrown - } - - @Test - void givenAProjectComponents_whenSaveProjectComponents_andBitbucketReject_ThenExceptionIsThrown() throws JsonProcessingException { - // given - var pathAt = mock(BitbucketPathAt.class); - var sourceCommitId = "sourceCommitId"; - var updatedProjectComponents = ProjectComponentsMother.of(); - var httpClientErrorException = new HttpClientErrorException(HttpStatus.CONFLICT, "Client Error"); - - doThrow(httpClientErrorException).when(bitbucketService).pushFile(eq(pathAt), eq(sourceCommitId), anyString()); - prepareMocksForSave(updatedProjectComponents); - - // when - var exception = assertThrows(HttpClientErrorException.class, () -> - provisionerActionsService.saveProjectComponents(pathAt, sourceCommitId, updatedProjectComponents) - ); - - // then - assertThat(exception).isEqualTo(httpClientErrorException); - } - - @Test - void givenExistingProjectComponents_whenUpdatePartiallyComponentProvisioningStatus_thenBitbucketFileIsUpdated() - throws JsonProcessingException { - - // given - var projectKey = "projectKey"; - var status = Status.FAILED; // any status is allowed - var componentId = "componentId"; - var catalogItemId = "catalogItemId"; - var componentUrl = "componentUrl"; - var parameterPair = Pair.of("paramName", List.of("paramValue")); - var workflowJobId = "workflowJobId"; - - var pathAt = BitbucketPathAtMother.of(); - var sourceCommitId = "sourceCommitId"; - - var projectComponents = ProjectComponentsMother.of(); - var updatedProjectComponents = ProjectComponentsMother.of(); - - // get path - prepareMocksForGetBitbucketPathAt(pathAt); - - // last commit exists - when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.of(sourceCommitId)); - - // project components exist - prepareMocksForGetExistingProjectComponents(pathAt, projectComponents); - - // partial update call - when(projectComponentsService.updatePartiallyExistingComponent( - projectComponents, - componentId, - catalogItemId, - status, - componentUrl, - workflowJobId, - List.of(Parameter.builder().name("paramName").values(List.of("paramValue")).build()) - )).thenReturn(updatedProjectComponents); - - var serializedUpdatedProjectComponents = prepareMocksForSave(updatedProjectComponents); - - // when - provisionerActionsService.updatePartiallyComponentProvisioningStatus( - projectKey, - status, - componentId, - catalogItemId, - componentUrl, - workflowJobId, - List.of(parameterPair) - ); - - // then - verify(bitbucketService).pushFile( - pathAt, - sourceCommitId, - serializedUpdatedProjectComponents - ); - } - - @Test - void givenNoProjectComponents_whenUpdatePartiallyComponentProvisioningStatus_thenThrowException(){ - - // given - var projectKey = "projectKey"; - var status = Status.FAILED; - var componentId = "componentId"; - var catalogItemId = "catalogItemId"; - var componentUrl = "componentUrl"; - var workflowJobId = "workflowJobId"; - - var pathAt = BitbucketPathAtMother.of(); - - prepareMocksForGetBitbucketPathAt(pathAt); - - // Simulate null projectComponents - when(bitbucketService.getTextFileContents(pathAt)).thenReturn(Optional.empty()); - when(projectComponentsService.createNewComponent()).thenReturn(null); - - // when - var exception = assertThrows(ElementNotFoundException.class, () -> - provisionerActionsService.updatePartiallyComponentProvisioningStatus( - projectKey, - status, - componentId, - catalogItemId, - componentUrl, - workflowJobId, - List.of() - ) - ); - - // then - assertThat(exception.getMessage()) - .isEqualTo("In a partial update, the projectComponent should exist."); - } - - @Test - void givenNewFile_whenUpdatePartiallyComponentProvisioningStatus_thenPushFileIsCalledWithNullCommit() - throws JsonProcessingException { - - // given - var projectKey = "projectKey"; - var status = Status.UNKNOWN; - var componentId = "componentId"; - var catalogItemId = "catalogItemId"; - var componentUrl = "url"; - var workflowJobId = "workflowJobId"; - - var pathAt = BitbucketPathAtMother.of(); - - var projectComponents = ProjectComponentsMother.of(); - var updatedProjectComponents = ProjectComponentsMother.of(); - - prepareMocksForGetBitbucketPathAt(pathAt); - - // no last commit - when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.empty()); - - // components exist - prepareMocksForGetExistingProjectComponents(pathAt, projectComponents); - - when(projectComponentsService.updatePartiallyExistingComponent( - eq(projectComponents), - eq(componentId), - eq(catalogItemId), - eq(status), - eq(componentUrl), - eq(workflowJobId), - any() - )).thenReturn(updatedProjectComponents); - - var serialized = prepareMocksForSave(updatedProjectComponents); - - // when - provisionerActionsService.updatePartiallyComponentProvisioningStatus( - projectKey, - status, - componentId, - catalogItemId, - componentUrl, - workflowJobId, - List.of() - ); - - // then - verify(bitbucketService).pushFile(pathAt, null, serialized); - } - - private String prepareMocksForSave(ProjectComponents updatedProjectComponents) throws JsonProcessingException { - var serializedUpdatedProjectComponents = "{ serialized-updated-json: true }"; - - when(objectMapper.writerWithDefaultPrettyPrinter()).thenReturn(objectWriter); - when(objectWriter.writeValueAsString(updatedProjectComponents)).thenReturn(serializedUpdatedProjectComponents); - - return serializedUpdatedProjectComponents; - } - - private void prepareMocksForGetNonExistingProjectComponents(BitbucketPathAt bitbucketPathAt, ProjectComponents projectComponents){ - when(bitbucketService.getTextFileContents(bitbucketPathAt)).thenReturn(Optional.empty()); - when(projectComponentsService.createNewComponent()).thenReturn(projectComponents); - } - - private void prepareMocksForGetExistingProjectComponents(BitbucketPathAt bitbucketPathAt, ProjectComponents projectComponents) throws JsonProcessingException { - var serializedProjectComponents = "{ serialized-json: true }"; - var bitbucketFileContent = Pair.of(MediaType.APPLICATION_JSON, serializedProjectComponents); - - when(bitbucketService.getTextFileContents(bitbucketPathAt)).thenReturn(Optional.of(bitbucketFileContent)); - when(objectMapper.readValue(serializedProjectComponents, ProjectComponents.class)).thenReturn(projectComponents); - } - - private void populateProvisionerActionsConfiguration() { - var projectKey = "configuredProjectKey"; - var repoSlug = "repoSlug"; - var subPath = "subPath"; - var subPathToken = "subPathToken"; - var branchName = "branchName"; - - provisionerActionsConfiguration.setProjectKey(projectKey); - provisionerActionsConfiguration.setRepositorySlug(repoSlug); - provisionerActionsConfiguration.setSubPath(subPath); - provisionerActionsConfiguration.setSubPathToken(subPathToken); - provisionerActionsConfiguration.setBranchName(branchName); - } - - private void prepareMocksForGetBitbucketPathAt(BitbucketPathAt bitbucketPathAt) { - when(bitbucketService.pathAtBuilder()).thenReturn(bitbucketPathAtBuilder); - when(bitbucketPathAtBuilder.projectKey(provisionerActionsConfiguration.getProjectKey())).thenReturn(bitbucketPathAtBuilder); - when(bitbucketPathAtBuilder.repoSlug(provisionerActionsConfiguration.getRepositorySlug())).thenReturn(bitbucketPathAtBuilder); - when(bitbucketPathAtBuilder.subPath(provisionerActionsConfiguration.getSubPath())).thenReturn(bitbucketPathAtBuilder); - when(bitbucketPathAtBuilder.at(provisionerActionsConfiguration.getBranchName())).thenReturn(bitbucketPathAtBuilder); - when(bitbucketPathAtBuilder.build()).thenReturn(bitbucketPathAt); - } -} +package org.opendevstack.component_catalog.server.services; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opendevstack.component_catalog.config.ProvisionerActionsConfiguration; +import org.opendevstack.component_catalog.server.controllers.exceptions.RestEntityNotFoundException; +import org.opendevstack.component_catalog.server.mother.BitbucketPathAtMother; +import org.opendevstack.component_catalog.server.mother.ProjectComponentsMother; +import org.opendevstack.component_catalog.server.services.bitbucket.BitbucketPathAt; +import org.opendevstack.component_catalog.server.services.exceptions.ComponentAlreadyExistsException; +import org.opendevstack.component_catalog.server.services.exceptions.ElementNotFoundException; +import org.opendevstack.component_catalog.server.services.exceptions.InvalidEntityException; +import org.opendevstack.component_catalog.server.services.provisioner.*; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.web.client.HttpClientErrorException; + +import java.util.Base64; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ProvisionerActionsServiceTest { + + @Mock + private BitbucketService bitbucketService; + + @Mock + private ProjectComponentsService projectComponentsService; + + @Mock + private ObjectMapper objectMapper; + + @Mock + private ObjectWriter objectWriter; + + @Mock + private BitbucketPathAt.BitbucketPathAtBuilder bitbucketPathAtBuilder; + + private ProvisionerActionsConfiguration provisionerActionsConfiguration; + + private ProvisionerActionsService provisionerActionsService; + + // helper + private ProjectComponentRequest request(String componentId, + String catalogItemId, + Status status, + String url, + String workflowJobId, + String createdAt, + String updatedAt, + List params) { + return ProjectComponentRequest.builder() + .componentId(componentId) + .catalogItemId(catalogItemId) + .status(status) + .componentUrl(url) + .workflowJobId(workflowJobId) + .createdAt(createdAt) + .updatedAt(updatedAt) + .parameters(params) + .build(); + } + + @BeforeEach + void setUp() { + provisionerActionsConfiguration = new ProvisionerActionsConfiguration(); + populateProvisionerActionsConfiguration(); + + provisionerActionsService = new ProvisionerActionsService(bitbucketService, objectMapper, projectComponentsService, provisionerActionsConfiguration); + } + + @Test + void givenAProvisionersObject_andCreatingStatus_whenValidate_andComponentIdExists_thenThrowException() { + // given + var componentId = "componentId"; + var status = Status.CREATING; + + ProjectComponents projectComponents = ProjectComponentsMother.of(); + + + // when + var exception = assertThrows(ComponentAlreadyExistsException.class, () -> + provisionerActionsService.validate(projectComponents, componentId, status) + ); + + // then + assertThat(exception.getMessage()).isEqualTo("Component with id 'componentId' already exists in the project components."); + } + + @Test + void givenAProvisionersObject_andCreatingStatus_whenUpdateComponentProvisioningStatus_thenBitbucketFileIsUpdated() throws JsonProcessingException { + // given + var projectKey = "projectKey"; + var status = Status.CREATING; + var componentId = "componentId"; + var catalogItemId = "catalogItemId"; + var componentUrl = "catalogUrl"; + var createdAt = "created"; + var updatedAt = "updated"; + + var pathAt = BitbucketPathAtMother.of(); + var sourceCommitId = "sourceCommitId"; + + var projectComponents = new ProjectComponents(); + var updatedProjectComponents = ProjectComponentsMother.of(); + + var parameters = List.of(Parameter.builder().name("parameterName").values(List.of("parameterValue")).build()); + + prepareMocksForGetBitbucketPathAt(pathAt); + when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.of(sourceCommitId)); + + prepareMocksForGetNonExistingProjectComponents(pathAt, projectComponents); + when(projectComponentsService.addNewComponent( + eq(projectComponents), + any(ProjectComponentRequest.class) + )).thenReturn(updatedProjectComponents); + + var serializedUpdatedProjectComponents = prepareMocksForSave(); + + // when + provisionerActionsService.updateComponentProvisioningStatus( + projectKey, + request(componentId, catalogItemId, status, componentUrl, null, createdAt, updatedAt, parameters) + ); + + // then + verify(bitbucketService).pushFile( + pathAt, + sourceCommitId, + serializedUpdatedProjectComponents + ); + } + + @ParameterizedTest + @EnumSource(value = Status.class, names = { "CREATED", "FAILED", "DELETING", "UNKNOWN" }) + void givenAProvisionersObject_andSelectedStatuses_whenUpdateComponentProvisioningStatus_thenBitbucketFileIsUpdated( + Status status + ) throws JsonProcessingException { + // given + var projectKey = "projectKey"; + var componentId = "componentId"; + var catalogItemId = "catalogItemId"; + var componentUrl = "catalogUrl"; + var createdAt = "created"; + var updatedAt = "updated"; + + var pathAt = BitbucketPathAtMother.of(); + var sourceCommitId = "sourceCommitId"; + + var projectComponents = ProjectComponentsMother.of(); + var updatedProjectComponents = ProjectComponentsMother.of(); + + prepareMocksForGetBitbucketPathAt(pathAt); + when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.of(sourceCommitId)); + + prepareMocksForGetExistingProjectComponents(pathAt, projectComponents); + when(projectComponentsService.updateExistingComponent( + eq(projectComponents), + any(ProjectComponentRequest.class) + )).thenReturn(updatedProjectComponents); + + var serializedUpdatedProjectComponents = prepareMocksForSave(); + + // when + provisionerActionsService.updateComponentProvisioningStatus( + projectKey, + request(componentId, catalogItemId, status, componentUrl, null, createdAt, updatedAt, List.of()) + ); + + // then + verify(bitbucketService).pushFile( + pathAt, + sourceCommitId, + serializedUpdatedProjectComponents + ); + } + + @Test + void givenASetOfProjectComponents_andACatalogItemId_andCatalogItemIdInComponents_whenIsProvisioned_thenReturnTrue() throws InvalidEntityException { + // given + var rawId = "my-item"; + var rawIdWithBranch = "my-item?at=refs/heads/main"; + + var encodedId = Base64.getEncoder().encodeToString(rawId.getBytes()); + var encodedIdWithBranch = Base64.getEncoder().encodeToString(rawIdWithBranch.getBytes()); + + var matchingComponent = new ProjectComponent(); + matchingComponent.setCatalogItemId(encodedId); + + var dummy1 = new ProjectComponent(); + dummy1.setCatalogItemId(Base64.getEncoder().encodeToString("dummy-1".getBytes())); + + var dummy2 = new ProjectComponent(); + dummy2.setCatalogItemId(Base64.getEncoder().encodeToString("dummy-2".getBytes())); + + var components = new HashMap(); + components.put("c1", dummy1); + components.put("c2", matchingComponent); // only this one matches + components.put("c3", dummy2); + + var projectComponents = new ProjectComponents(); + projectComponents.setComponents(components); + + when(projectComponentsService.getRepoPathFromCatalogItemId(anyString())).thenCallRealMethod(); + + // when + var result = provisionerActionsService.isProvisioned(projectComponents, encodedIdWithBranch); + + // then + assertThat(result).isTrue(); + } + + @Test + void givenASetOfProjectComponents_andACatalogItemId_andCatalogItemIdNotInComponents_whenIsProvisioned_thenReturnFalse() throws InvalidEntityException { + // given + var rawId = "my-item"; + var encodedId = Base64.getEncoder().encodeToString(rawId.getBytes()); + + var dummy1 = new ProjectComponent(); + dummy1.setCatalogItemId(Base64.getEncoder().encodeToString("dummy-1".getBytes())); + + var dummy2 = new ProjectComponent(); + dummy2.setCatalogItemId(Base64.getEncoder().encodeToString("dummy-2".getBytes())); + + var components = new HashMap(); + components.put("c1", dummy1); + components.put("c2", dummy2); + + var projectComponents = new ProjectComponents(); + projectComponents.setComponents(components); + + when(projectComponentsService.getRepoPathFromCatalogItemId(anyString())).thenCallRealMethod(); + + // when + var result = provisionerActionsService.isProvisioned(projectComponents, encodedId); + + // then + assertThat(result).isFalse(); + } + + @Test + void givenAProjectKey_andAComponentId_whenDeleteComponentProvisioningStatus_thenBitbucketServiceIsCalled() throws JsonProcessingException { + // given + var projectKey = "projectKey"; + var componentId = "componentId"; + + var pathAtBuilder = mock(BitbucketPathAt.BitbucketPathAtBuilder.class); + var pathAt = mock(BitbucketPathAt.class); + var sourceCommitId = "sourceCommitId"; + + var projectComponents = ProjectComponentsMother.of(); + var projectComponentsWithoutComponentId = ProjectComponentsMother.of(); + + when(bitbucketService.pathAtBuilder()).thenReturn(pathAtBuilder); + when(pathAtBuilder.projectKey(any())).thenReturn(pathAtBuilder); + when(pathAtBuilder.repoSlug(any())).thenReturn(pathAtBuilder); + when(pathAtBuilder.subPath(any())).thenReturn(pathAtBuilder); + when(pathAtBuilder.at(any())).thenReturn(pathAtBuilder); + when(pathAtBuilder.build()).thenReturn(pathAt); + + when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.of(sourceCommitId)); + when(projectComponentsService.deleteComponent(projectComponents, componentId)).thenReturn(projectComponentsWithoutComponentId); + + prepareMocksForGetExistingProjectComponents(pathAt, projectComponents); + var serializedProjectComponentsWithoutComponentId = prepareMocksForSave(); + + // when + provisionerActionsService.deleteComponentProvisioningStatus(projectKey, componentId); + + // then + verify(bitbucketService).pushFile(pathAt, sourceCommitId, serializedProjectComponentsWithoutComponentId); + } + + @Test + void givenAProjectKey_andAComponentId_whenDeleteComponentProvisioningStatus_andNoProjectComponentsForProjectKey_thenExceptionIsThrown() { + // given + var projectKey = "projectKey"; + var componentId = "componentId"; + + var pathAtBuilder = mock(BitbucketPathAt.BitbucketPathAtBuilder.class); + var pathAt = mock(BitbucketPathAt.class); + + var projectComponents = ProjectComponentsMother.of(); + + when(bitbucketService.pathAtBuilder()).thenReturn(pathAtBuilder); + when(pathAtBuilder.projectKey(any())).thenReturn(pathAtBuilder); + when(pathAtBuilder.repoSlug(any())).thenReturn(pathAtBuilder); + when(pathAtBuilder.subPath(any())).thenReturn(pathAtBuilder); + when(pathAtBuilder.at(any())).thenReturn(pathAtBuilder); + when(pathAtBuilder.build()).thenReturn(pathAt); + + when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.empty()); + + assertThat(projectComponents.getComponents()).containsKey(componentId); + + // when + var exception = assertThrows(RestEntityNotFoundException.class, () -> + provisionerActionsService.deleteComponentProvisioningStatus(projectKey, componentId) + ); + + // then + assertThat(exception.getMessage()).startsWith("No component provisioning status for pathAt:"); + verify(bitbucketService, times(0)).pushFile(any(), any(), anyString()); + } + + @Test + void givenAProjectComponents_whenSaveProjectComponents_andBitbucketRejectAsNoUpdates_ThenExceptionIsIgnored() throws JsonProcessingException { + // given + var pathAt = mock(BitbucketPathAt.class); + var sourceCommitId = "sourceCommitId"; + var updatedProjectComponents = ProjectComponentsMother.of(); + var httpClientErrorException = new HttpClientErrorException(HttpStatus.CONFLICT, "\"{\"errors\":" + + "[{\"context\":null,\"message\":\"The content provided is the same as what already exists. No change was committed.\"" + + ",\"exceptionName\":\"com.atlassian.bitbucket.content.FileContentUnmodifiedException\"}]}\""); + + doThrow(httpClientErrorException).when(bitbucketService).pushFile(eq(pathAt), eq(sourceCommitId), anyString()); + prepareMocksForSave(); + + // when + provisionerActionsService.saveProjectComponents(pathAt, sourceCommitId, updatedProjectComponents); + + // then + // no exception is thrown + } + + @Test + void givenAProjectComponents_whenSaveProjectComponents_andBitbucketReject_ThenExceptionIsThrown() throws JsonProcessingException { + // given + var pathAt = mock(BitbucketPathAt.class); + var sourceCommitId = "sourceCommitId"; + var updatedProjectComponents = ProjectComponentsMother.of(); + var httpClientErrorException = new HttpClientErrorException(HttpStatus.CONFLICT, "Client Error"); + + doThrow(httpClientErrorException).when(bitbucketService).pushFile(eq(pathAt), eq(sourceCommitId), anyString()); + prepareMocksForSave(); + + // when + var exception = assertThrows(HttpClientErrorException.class, () -> + provisionerActionsService.saveProjectComponents(pathAt, sourceCommitId, updatedProjectComponents) + ); + + // then + assertThat(exception).isEqualTo(httpClientErrorException); + } + + @Test + void givenExistingProjectComponents_whenUpdatePartiallyComponentProvisioningStatus_thenBitbucketFileIsUpdated() + throws JsonProcessingException { + + // given + var projectKey = "projectKey"; + var status = Status.FAILED; // any status is allowed + var componentId = "componentId"; + var catalogItemId = "catalogItemId"; + var componentUrl = "componentUrl"; + var parameterList = List.of(Parameter.builder().name("paramName").values(List.of("paramValue")).build()); + var workflowJobId = "workflowJobId"; + var createdAt = "created"; + var updatedAt = "updated"; + + var pathAt = BitbucketPathAtMother.of(); + var sourceCommitId = "sourceCommitId"; + + var projectComponents = ProjectComponentsMother.of(); + var updatedProjectComponents = ProjectComponentsMother.of(); + + // get path + prepareMocksForGetBitbucketPathAt(pathAt); + + // last commit exists + when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.of(sourceCommitId)); + + // project components exist + prepareMocksForGetExistingProjectComponents(pathAt, projectComponents); + + // partial update call + when(projectComponentsService.updatePartiallyExistingComponent( + eq(projectComponents), + any(ProjectComponentRequest.class) + )).thenReturn(updatedProjectComponents); + + var serializedUpdatedProjectComponents = prepareMocksForSave(); + + // when + provisionerActionsService.updatePartiallyComponentProvisioningStatus( + projectKey, + request(componentId, catalogItemId, status, componentUrl, workflowJobId, createdAt, updatedAt, parameterList) + ); + + // then + verify(bitbucketService).pushFile( + pathAt, + sourceCommitId, + serializedUpdatedProjectComponents + ); + } + + @Test + void givenNoProjectComponents_whenUpdatePartiallyComponentProvisioningStatus_thenThrowException(){ + + // given + var projectKey = "projectKey"; + var status = Status.FAILED; + var componentId = "componentId"; + var catalogItemId = "catalogItemId"; + var componentUrl = "componentUrl"; + var workflowJobId = "workflowJobId"; + var createdAt = "created"; + var updatedAt = "updated"; + + var pathAt = BitbucketPathAtMother.of(); + + prepareMocksForGetBitbucketPathAt(pathAt); + + // Simulate null projectComponents + when(bitbucketService.getTextFileContents(pathAt)).thenReturn(Optional.empty()); + when(projectComponentsService.createNewComponent()).thenReturn(null); + + // when + var exception = assertThrows(ElementNotFoundException.class, () -> + provisionerActionsService.updatePartiallyComponentProvisioningStatus( + projectKey, + request(componentId, catalogItemId, status, componentUrl, workflowJobId, createdAt, updatedAt, List.of()) + ) + ); + + // then + assertThat(exception.getMessage()) + .isEqualTo("In a partial update, the projectComponent should exist."); + } + + @Test + void givenNewFile_whenUpdatePartiallyComponentProvisioningStatus_thenPushFileIsCalledWithNullCommit() + throws JsonProcessingException { + + // given + var projectKey = "projectKey"; + var status = Status.UNKNOWN; + var componentId = "componentId"; + var catalogItemId = "catalogItemId"; + var componentUrl = "url"; + var workflowJobId = "workflowJobId"; + var createdAt = "created"; + var updatedAt = "updated"; + + var pathAt = BitbucketPathAtMother.of(); + + var projectComponents = ProjectComponentsMother.of(); + var updatedProjectComponents = ProjectComponentsMother.of(); + + prepareMocksForGetBitbucketPathAt(pathAt); + + // no last commit + when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.empty()); + + // components exist + prepareMocksForGetExistingProjectComponents(pathAt, projectComponents); + + when(projectComponentsService.updatePartiallyExistingComponent( + eq(projectComponents), + any(ProjectComponentRequest.class) + )).thenReturn(updatedProjectComponents); + + var serialized = prepareMocksForSave(); + + // when + provisionerActionsService.updatePartiallyComponentProvisioningStatus( + projectKey, + request(componentId, catalogItemId, status, componentUrl, workflowJobId, createdAt, updatedAt, List.of()) + ); + + // then + verify(bitbucketService).pushFile(pathAt, null, serialized); + } + @Test + void givenExistingComponent_whenUpdate_thenCreatedAtIsPreserved() throws Exception { + // given + var componentId = "componentId"; + + var existing = new ProjectComponent(); + existing.setCreatedAt("originalCreatedAt"); + + var components = new HashMap(); + components.put(componentId, existing); + + var projectComponents = new ProjectComponents(); + projectComponents.setComponents(components); + + var pathAt = BitbucketPathAtMother.of(); + + prepareMocksForGetBitbucketPathAt(pathAt); + when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.of("commit")); + prepareMocksForGetExistingProjectComponents(pathAt, projectComponents); + + when(projectComponentsService.updateExistingComponent( + any(), any(ProjectComponentRequest.class) + )).thenReturn(ProjectComponentsMother.of()); + + prepareMocksForSave(); + + // when + provisionerActionsService.updateComponentProvisioningStatus( + "projectKey", + request(componentId, "catalogItemId", Status.CREATED, "url", null, "created", "updated", List.of()) + ); + + verify(projectComponentsService).updateExistingComponent( + eq(projectComponents), + argThat(req -> "originalCreatedAt".equals(req.getCreatedAt())) + ); + } + + @Test + void givenExistingComponent_whenUpdate_thenUpdatedAtIsGenerated() throws Exception { + // given + var componentId = "componentId"; + + var existing = new ProjectComponent(); + existing.setCreatedAt("originalCreatedAt"); + + var components = new HashMap(); + components.put(componentId, existing); + + var projectComponents = new ProjectComponents(); + projectComponents.setComponents(components); + + var pathAt = BitbucketPathAtMother.of(); + + prepareMocksForGetBitbucketPathAt(pathAt); + when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.of("commit")); + prepareMocksForGetExistingProjectComponents(pathAt, projectComponents); + + when(projectComponentsService.updateExistingComponent( + any(), any(ProjectComponentRequest.class) + )).thenReturn(ProjectComponentsMother.of()); + + prepareMocksForSave(); + + // when + provisionerActionsService.updateComponentProvisioningStatus( + "projectKey", + request(componentId, "catalogItemId", Status.CREATED, "url", null, "created", "updated", List.of()) + ); + + // then + verify(projectComponentsService).updateExistingComponent( + any(), + argThat(req -> { + try { + return Long.parseLong(req.getUpdatedAt()) > 0; + } catch (Exception e) { + return false; + } + }) + ); + } + + @Test + void givenExistingComponent_whenPartialUpdate_thenUpdatedAtIsModified() throws Exception { + // given + var componentId = "componentId"; + + var component = new ProjectComponent(); + component.setUpdatedAt("old"); + + var map = new HashMap(); + map.put(componentId, component); + + var projectComponents = new ProjectComponents(); + projectComponents.setComponents(map); + + var pathAt = BitbucketPathAtMother.of(); + + prepareMocksForGetBitbucketPathAt(pathAt); + when(bitbucketService.getLastCommit(pathAt)).thenReturn(Optional.of("commit")); + + prepareMocksForGetExistingProjectComponents(pathAt, projectComponents); + + when(projectComponentsService.updatePartiallyExistingComponent( + any(), + any(ProjectComponentRequest.class) + )).thenReturn(ProjectComponentsMother.of()); + + prepareMocksForSave(); + + // when + provisionerActionsService.updatePartiallyComponentProvisioningStatus( + "projectKey", + request(componentId, "catalogItemId", Status.FAILED, "url", "jobId", "created", "updated", List.of()) + ); + + // then + assertThat(Long.parseLong(component.getUpdatedAt())).isGreaterThan(0); + } + + private String prepareMocksForSave() throws JsonProcessingException { + var serializedUpdatedProjectComponents = "{ serialized-updated-json: true }"; + + when(objectMapper.writerWithDefaultPrettyPrinter()).thenReturn(objectWriter); + when(objectWriter.writeValueAsString(any())).thenReturn(serializedUpdatedProjectComponents); + + return serializedUpdatedProjectComponents; + } + + private void prepareMocksForGetNonExistingProjectComponents(BitbucketPathAt bitbucketPathAt, ProjectComponents projectComponents){ + when(bitbucketService.getTextFileContents(bitbucketPathAt)).thenReturn(Optional.empty()); + when(projectComponentsService.createNewComponent()).thenReturn(projectComponents); + } + + private void prepareMocksForGetExistingProjectComponents(BitbucketPathAt bitbucketPathAt, ProjectComponents projectComponents) throws JsonProcessingException { + var serializedProjectComponents = "{ serialized-json: true }"; + var bitbucketFileContent = Pair.of(MediaType.APPLICATION_JSON, serializedProjectComponents); + + when(bitbucketService.getTextFileContents(bitbucketPathAt)).thenReturn(Optional.of(bitbucketFileContent)); + when(objectMapper.readValue(serializedProjectComponents, ProjectComponents.class)).thenReturn(projectComponents); + } + + private void populateProvisionerActionsConfiguration() { + var projectKey = "configuredProjectKey"; + var repoSlug = "repoSlug"; + var subPath = "subPath"; + var subPathToken = "subPathToken"; + var branchName = "branchName"; + + provisionerActionsConfiguration.setProjectKey(projectKey); + provisionerActionsConfiguration.setRepositorySlug(repoSlug); + provisionerActionsConfiguration.setSubPath(subPath); + provisionerActionsConfiguration.setSubPathToken(subPathToken); + provisionerActionsConfiguration.setBranchName(branchName); + } + + private void prepareMocksForGetBitbucketPathAt(BitbucketPathAt bitbucketPathAt) { + when(bitbucketService.pathAtBuilder()).thenReturn(bitbucketPathAtBuilder); + when(bitbucketPathAtBuilder.projectKey(provisionerActionsConfiguration.getProjectKey())).thenReturn(bitbucketPathAtBuilder); + when(bitbucketPathAtBuilder.repoSlug(provisionerActionsConfiguration.getRepositorySlug())).thenReturn(bitbucketPathAtBuilder); + when(bitbucketPathAtBuilder.subPath(provisionerActionsConfiguration.getSubPath())).thenReturn(bitbucketPathAtBuilder); + when(bitbucketPathAtBuilder.at(provisionerActionsConfiguration.getBranchName())).thenReturn(bitbucketPathAtBuilder); + when(bitbucketPathAtBuilder.build()).thenReturn(bitbucketPathAt); + } +} \ No newline at end of file