diff --git a/api-project-component-v0/openapi/api-project-component-v0-internal.yaml b/api-project-component-v0/openapi/api-project-component-v0-internal.yaml new file mode 100644 index 0000000..a903d6e --- /dev/null +++ b/api-project-component-v0/openapi/api-project-component-v0-internal.yaml @@ -0,0 +1,66 @@ +openapi: 3.0.3 +info: + title: ODS API Server + description: API documentation for ODS (Open DevStack) API Service + contact: + name: ODS Team + version: v0.0.1 +servers: + - url: http://{baseurl}/api/pub/v1 + variables: + baseurl: + default: localhost:8080 + description: Development environment +tags: + - name: Project Components Internal + description: API for managing project components with extended methods + +paths: + /projects/{projectId}/components/{componentId}: + delete: + tags: + - Project Components Internal + summary: Delete component information + operationId: deleteProjectComponent + description: Deletes a specific component + parameters: + - name: projectId + in: path + required: true + description: Project key + schema: + type: string + pattern: "^[A-Z]{2}[A-Z0-9]{1,8}$" + minLength: 3 + maxLength: 10 + - name: componentId + in: path + required: true + description: Component id + schema: + type: string + minLength: 2 + maxLength: 52 + pattern: "^[a-z]+(-?[a-z0-9]+)*$" + responses: + "204": + description: Project component properly deleted or no component found for the provided componentId. + "400": + description: Bad request. + "401": + description: Invalid credentials on the request. + "403": + description: Insufficient permissions for the client to access the resource. + "500": + description: Server error. + +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + diff --git a/api-project-component-v0/pom.xml b/api-project-component-v0/pom.xml index 35f3ddf..495e438 100644 --- a/api-project-component-v0/pom.xml +++ b/api-project-component-v0/pom.xml @@ -161,6 +161,35 @@ + + generate-api-project-component-extended + + generate + + + spring + ${project.basedir} + spring-boot + ${project.basedir}/openapi/api-project-component-v0-internal.yaml + org.opendevstack.apiservice.project.api + org.opendevstack.apiservice.project.model + org.opendevstack.apiservice.project + false + false + false + false + false + false + + true + true + springdoc + true + true + true + + + diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsInternalController.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsInternalController.java new file mode 100644 index 0000000..63c1f3d --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsInternalController.java @@ -0,0 +1,29 @@ +package org.opendevstack.apiservice.project.controller; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.project.api.ProjectComponentsInternalApi; +import org.opendevstack.apiservice.project.facade.ComponentsFacade; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@AllArgsConstructor +@Slf4j +@RequestMapping(ProjectComponentsInternalController.API_BASE_PATH) +public class ProjectComponentsInternalController implements ProjectComponentsInternalApi { + + public static final String API_BASE_PATH = "/api/pub/v1"; + + private final ComponentsFacade componentsFacade; + + @Override + public ResponseEntity deleteProjectComponent(String projectId, String componentId) { + componentsFacade.deleteProjectComponent(projectId, componentId); + log.info("Deleted component with id '{}' for project '{}'", componentId, projectId); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + +} diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandler.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandler.java index add5c7f..5af8579 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandler.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandler.java @@ -7,10 +7,10 @@ import org.opendevstack.apiservice.project.exception.ComponentAlreadyExistsException; import org.opendevstack.apiservice.project.exception.ComponentBadRequestException; import org.opendevstack.apiservice.project.exception.ComponentCreationException; +import org.opendevstack.apiservice.project.exception.ComponentDeletionException; import org.opendevstack.apiservice.project.exception.ComponentErrorKey; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; import org.opendevstack.apiservice.project.exception.ComponentRetrievalException; -import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.CreateComponentResponse; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -142,6 +142,16 @@ public ResponseEntity handleComponentCreationException( return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); } + @ExceptionHandler(ComponentDeletionException.class) + public ResponseEntity handleComponentDeletionException( + ComponentDeletionException ex, + HttpServletRequest request) { + + log.error("Component deletion failed: {}", ex.getMessage(), ex); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); + } + @ExceptionHandler(ComponentRetrievalException.class) public ResponseEntity handleComponentRetrievalException( ComponentRetrievalException ex, diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentDeletionException.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentDeletionException.java new file mode 100644 index 0000000..f1eeffa --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentDeletionException.java @@ -0,0 +1,12 @@ +package org.opendevstack.apiservice.project.exception; + +public class ComponentDeletionException extends RuntimeException { + + public ComponentDeletionException(String message) { + super(message); + } + + public ComponentDeletionException(String message, Exception e) { + super(message, e); + } +} diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/facade/ComponentsFacade.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/facade/ComponentsFacade.java index 3888310..56cc6c3 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/facade/ComponentsFacade.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/facade/ComponentsFacade.java @@ -12,6 +12,7 @@ import org.opendevstack.apiservice.project.exception.ComponentAlreadyExistsException; import org.opendevstack.apiservice.project.exception.ComponentBadRequestException; import org.opendevstack.apiservice.project.exception.ComponentCreationException; +import org.opendevstack.apiservice.project.exception.ComponentDeletionException; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; import org.opendevstack.apiservice.project.exception.ComponentRetrievalException; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; @@ -144,13 +145,34 @@ private String extractHttpErrorMessage(Throwable throwable) { return throwable.getMessage(); } - public Boolean deleteProjectComponent(String projectId, String componentId) { + public void deleteProjectComponent(String projectId, String componentId) { try { - return marketplaceExternalService.deleteProjectComponent(projectId, componentId); + marketplaceExternalService.deleteProjectComponent(projectId, componentId); + log.info("Successfully deleted component '{}' for project '{}'", componentId, projectId); } catch (MarketplaceException e) { - log.error("Failed to delete component with id {} for project with id {}", componentId, projectId, e); - return false; + log.error("Failed to delete component '{}' for project '{}': {}", componentId, projectId, e.getMessage(), e); + // Check if it's an access denied error + if (isAccessDeniedCause(e)) { + throw new ComponentDeletionException( + String.format("Access denied when deleting component '%s' from project '%s'", componentId, projectId), e); + } + // Generic deletion failure + throw new ComponentDeletionException( + String.format("Failed to delete component '%s' for project '%s': %s", componentId, projectId, e.getMessage()), e + ); + } + } + + private boolean isAccessDeniedCause(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof HttpClientErrorException.Unauthorized + || current instanceof HttpClientErrorException.Forbidden) { + return true; + } + current = current.getCause(); } + return false; } public boolean registerProjectComponent(String projectId, String componentId) { diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsControllerTest.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsControllerTest.java index 8142c12..3f69e77 100644 --- a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsControllerTest.java +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsControllerTest.java @@ -107,4 +107,5 @@ void get_project_component_throws_not_found_when_component_does_not_exist() thro .hasMessage("Component '" + componentId + "' not found for project 'projectId'"); verify(componentsFacade).getProjectComponent(projectId, componentId); } + } \ No newline at end of file diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsInternalControllerTest.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsInternalControllerTest.java new file mode 100644 index 0000000..4554359 --- /dev/null +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsInternalControllerTest.java @@ -0,0 +1,64 @@ +package org.opendevstack.apiservice.project.controller; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.opendevstack.apiservice.project.exception.ComponentDeletionException; +import org.opendevstack.apiservice.project.facade.ComponentsFacade; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; + +class ProjectComponentsInternalControllerTest { + + @Mock + private ComponentsFacade componentsFacade; + + private ProjectComponentsInternalController projectComponentsInternalController; + + private AutoCloseable openMocks; + + @BeforeEach + void setup() { + openMocks = MockitoAnnotations.openMocks(this); + projectComponentsInternalController = new ProjectComponentsInternalController(componentsFacade); + } + + @AfterEach + void tearDown() throws Exception { + openMocks.close(); + } + + @Test + void delete_project_component_returns_no_content_when_component_delete_works() { + String projectId = "projectId"; + String componentId = "test-component-delete"; + + ResponseEntity response = projectComponentsInternalController.deleteProjectComponent(projectId, componentId); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + assertThat(response.getBody()).isNull(); + verify(componentsFacade).deleteProjectComponent(projectId, componentId); + } + + @Test + void delete_project_component_throws_exception_when_component_delete_api_throws_exception() { + String projectId = "projectId"; + String componentId = "test-component-delete"; + + doThrow(new ComponentDeletionException("test exception")) + .when(componentsFacade).deleteProjectComponent(anyString(), anyString()); + + assertThrows(ComponentDeletionException.class, () -> + projectComponentsInternalController.deleteProjectComponent(projectId, componentId) + ); + verify(componentsFacade).deleteProjectComponent(projectId, componentId); + } +} \ No newline at end of file diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/facade/ComponentsFacadeTest.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/facade/ComponentsFacadeTest.java index b8c7748..0507499 100644 --- a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/facade/ComponentsFacadeTest.java +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/facade/ComponentsFacadeTest.java @@ -12,6 +12,7 @@ import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; import org.opendevstack.apiservice.project.exception.ComponentAlreadyExistsException; import org.opendevstack.apiservice.project.exception.ComponentCreationException; +import org.opendevstack.apiservice.project.exception.ComponentDeletionException; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; import org.opendevstack.apiservice.project.model.Component; @@ -21,11 +22,12 @@ import org.springframework.http.HttpStatus; import org.springframework.web.client.HttpClientErrorException; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.opendevstack.apiservice.project.util.TestObjectsBuilder.buildTestCatalogItem; @@ -120,4 +122,23 @@ void create_project_component_throws_already_exists_when_marketplace_returns_con .hasMessage("This component name already exists, please choose another name."); verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), anyList()); } + + + @Test + void delete_project_component_ends_successfully_for_existing_component() throws MarketplaceException { + componentsFacade.deleteProjectComponent("testProject", "testComponent"); + + verify(marketplaceExternalService).deleteProjectComponent("testProject", "testComponent"); + } + + @Test + void delete_project_component_throws_component_deletion_exception_when_marketplace_exception_is_thrown() throws MarketplaceException { + doThrow(new MarketplaceException("Test exception")) + .when(marketplaceExternalService).deleteProjectComponent("testProject", "testComponent"); + + assertThatThrownBy(() -> componentsFacade.deleteProjectComponent("testProject", "testComponent")) + .isInstanceOf(ComponentDeletionException.class) + .hasMessageContaining("Test exception"); + verify(marketplaceExternalService).deleteProjectComponent("testProject", "testComponent"); + } } \ No newline at end of file diff --git a/external-service-marketplace/openapi/openapi-component_provisioner-v1.0.0.yaml b/external-service-marketplace/openapi/openapi-component_provisioner-v1.0.0.yaml index 4de634a..69e19ab 100644 --- a/external-service-marketplace/openapi/openapi-component_provisioner-v1.0.0.yaml +++ b/external-service-marketplace/openapi/openapi-component_provisioner-v1.0.0.yaml @@ -244,7 +244,7 @@ paths: schema: $ref: '#/components/schemas/ProvisioningDeleteRequest' responses: - "200": + "204": description: Project component properly deleted. "400": description: Bad request. diff --git a/external-service-marketplace/pom.xml b/external-service-marketplace/pom.xml index bdc0f96..a8799c6 100644 --- a/external-service-marketplace/pom.xml +++ b/external-service-marketplace/pom.xml @@ -186,7 +186,7 @@ generate - FILTER=operationId:triggerProvisionAction|notifyProvisioningStatusUpdate|createIncident|getProvisionerHealth + FILTER=operationId:triggerProvisionAction|notifyProvisioningStatusUpdate|deleteProvisioningStatus|getProvisionerHealth java ${project.basedir} resttemplate diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClient.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClient.java index 7796f68..6205674 100644 --- a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClient.java +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClient.java @@ -37,6 +37,12 @@ public MarketplaceApiClient(String instanceName, MarketplaceInstanceConfig confi // Initialize the generated ApiClient this.apiClient = new ApiClient(restTemplate); + if (config.getUsername() != null && config.getPassword() != null) { + this.apiClient.setUsername(config.getUsername()); + this.apiClient.setPassword(config.getPassword()); + log.info("MarketplaceApiClient for instance '{}' uses basic authentication", instanceName); + } + log.info("MarketplaceApiClient initialized for instance '{}'", instanceName); } diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClientFactory.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClientFactory.java index 093825d..3b6fed5 100644 --- a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClientFactory.java +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClientFactory.java @@ -34,7 +34,7 @@ public class MarketplaceApiClientFactory { * @param restTemplateBuilder RestTemplate builder for creating HTTP clients */ public MarketplaceApiClientFactory(MarketplaceServiceConfig configuration, - RestTemplateBuilder restTemplateBuilder) { + RestTemplateBuilder restTemplateBuilder) { this.configuration = configuration; this.restTemplateBuilder = restTemplateBuilder; @@ -70,7 +70,7 @@ public String getDefaultInstanceName() throws MarketplaceException { /** * Get a {@link MarketplaceApiClient} for a specific instance. - * If {@code instanceName} is {@code null} or blank, this method will throw a {@link MarketplaceException} + * If {@code instanceName} is {@code null} or blank, this method will throw a {@link MarketplaceException} * to avoid ambiguity. The caller should explicitly call {@link #getClient()} to get the default instance client * in that case. * @@ -212,4 +212,4 @@ protected void prepareConnection(HttpURLConnection connection, String httpMethod } }; } -} +} \ No newline at end of file diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceInstanceConfig.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceInstanceConfig.java index 7b170d2..9fb66a1 100644 --- a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceInstanceConfig.java +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceInstanceConfig.java @@ -14,6 +14,16 @@ public class MarketplaceInstanceConfig { */ private String provisionerActionsBaseUrl; + /** + * The username used for basic auth + */ + private String username; + + /** + * The password used for basic auth + */ + private String password; + /** * Connection timeout in milliseconds (default: 30000) */ diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/MarketplaceService.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/MarketplaceService.java index fc47b9b..29ffdf4 100644 --- a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/MarketplaceService.java +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/MarketplaceService.java @@ -33,9 +33,9 @@ public interface MarketplaceService extends ExternalService { boolean provisionProjectComponent(String instanceName, String projectId, List params) throws MarketplaceException; - boolean deleteProjectComponent(String projectId, String componentId) throws MarketplaceException; + void deleteProjectComponent(String projectId, String componentId) throws MarketplaceException; - boolean deleteProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException; + void deleteProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException; void registerProjectComponent(String projectId, String componentId) throws MarketplaceException; diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/impl/MarketplaceServiceImpl.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/impl/MarketplaceServiceImpl.java index 1e58a50..de7e36e 100644 --- a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/impl/MarketplaceServiceImpl.java +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/impl/MarketplaceServiceImpl.java @@ -15,7 +15,6 @@ import org.opendevstack.apiservice.externalservice.marketplace.openapi.api.ProvisionerActionsApi; import org.opendevstack.apiservice.externalservice.marketplace.openapi.api.ProvisionerHealthApi; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CreateIncidentAction; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.GetCatalogHealth200Response; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.GetProvisionerHealth200Response; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.NotifyProvisioningStatusUpdateRequest; @@ -23,6 +22,7 @@ import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionAction; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionResponse; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisioningDeleteRequest; import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; @@ -140,9 +140,9 @@ public boolean provisionProjectComponent(String projectId, List params) + List params) throws MarketplaceException { log.debug("Marketplace service PROVISION component for project {}: ", projectId); try { @@ -177,24 +177,24 @@ public boolean provisionProjectComponent(String instanceName, } @Override - public boolean deleteProjectComponent(String projectId, String componentId) throws MarketplaceException { - return deleteProjectComponent(getDefaultInstance(), projectId, componentId); + public void deleteProjectComponent(String projectId, String componentId) throws MarketplaceException { + deleteProjectComponent(getDefaultInstance(), projectId, componentId); } @Override - public boolean deleteProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException { + public void deleteProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException { log.debug("Marketplace service DELETE component {} for project {}: ", componentId, projectId); try { - MarketplaceApiClient marketplaceClient = getOboAuthenticatedClient(instanceName); + MarketplaceApiClient marketplaceClient = clientFactory.getClient(instanceName); ApiClient apiClient = marketplaceClient.getApiClient(); ProvisionResultsApi provisionResultsApi = new ProvisionResultsApi(apiClient); apiClient.setBasePath(marketplaceClient.getConfig().getProvisionerActionsBaseUrl()); log.debug("Api client base path: {}", apiClient.getBasePath()); - CreateIncidentAction deleteAction = new CreateIncidentAction(); - ProvisionActionResponse response = provisionResultsApi.createIncident(projectId, componentId, deleteAction); - return !Boolean.TRUE.equals(response.getFailed()); + ProvisioningDeleteRequest provisioningDeleteRequest = new ProvisioningDeleteRequest(); + provisioningDeleteRequest.setComponentId(componentId); + provisionResultsApi.deleteProvisioningStatus(projectId, provisioningDeleteRequest); } catch (HttpClientErrorException.Unauthorized | HttpClientErrorException.Forbidden e) { throw new MarketplaceException( String.format("Access denied when deleting project component '%s' in project '%s' and instance '%s'", diff --git a/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/service/MarketplaceServiceImplTest.java b/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/service/MarketplaceServiceImplTest.java index 4f6749a..03fb26e 100644 --- a/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/service/MarketplaceServiceImplTest.java +++ b/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/service/MarketplaceServiceImplTest.java @@ -209,88 +209,88 @@ void testGetProjectComponent_NotFound_ReturnsNull() throws MarketplaceException // ------------------------------------------------------------------------- @Test - void testIsHealthy_NoInstancesConfigured_ReturnsFalse() { - when(clientFactory.getAvailableInstances()).thenReturn(Set.of()); + void testIsHealthy_NoInstancesConfigured_ReturnsFalse() { + when(clientFactory.getAvailableInstances()).thenReturn(Set.of()); - MarketplaceServiceImpl service = new MarketplaceServiceImpl(clientFactory, oboTokenService) { - @Override - protected boolean isProvisionerEndpointUp(MarketplaceApiClient marketplaceClient) { - return true; - } + MarketplaceServiceImpl service = new MarketplaceServiceImpl(clientFactory, oboTokenService) { + @Override + protected boolean isProvisionerEndpointUp(MarketplaceApiClient marketplaceClient) { + return true; + } - @Override - protected boolean isCatalogEndpointUp(MarketplaceApiClient marketplaceClient) { - return true; - } - }; + @Override + protected boolean isCatalogEndpointUp(MarketplaceApiClient marketplaceClient) { + return true; + } + }; - boolean result = service.isHealthy(); + boolean result = service.isHealthy(); - assertFalse(result); + assertFalse(result); } @Test - void testIsHealthy_BothEndpointsUp_ReturnsTrue() throws MarketplaceException { - when(clientFactory.getAvailableInstances()).thenReturn(Set.of("dev")); - when(clientFactory.getDefaultInstanceName()).thenReturn("dev"); - when(clientFactory.getClient("dev")).thenReturn(marketplaceApiClient); + void testIsHealthy_BothEndpointsUp_ReturnsTrue() throws MarketplaceException { + when(clientFactory.getAvailableInstances()).thenReturn(Set.of("dev")); + when(clientFactory.getDefaultInstanceName()).thenReturn("dev"); + when(clientFactory.getClient("dev")).thenReturn(marketplaceApiClient); - MarketplaceServiceImpl service = new MarketplaceServiceImpl(clientFactory, oboTokenService) { - @Override - protected boolean isProvisionerEndpointUp(MarketplaceApiClient marketplaceClient) { - return true; - } + MarketplaceServiceImpl service = new MarketplaceServiceImpl(clientFactory, oboTokenService) { + @Override + protected boolean isProvisionerEndpointUp(MarketplaceApiClient marketplaceClient) { + return true; + } - @Override - protected boolean isCatalogEndpointUp(MarketplaceApiClient marketplaceClient) { - return true; - } - }; + @Override + protected boolean isCatalogEndpointUp(MarketplaceApiClient marketplaceClient) { + return true; + } + }; - boolean result = service.isHealthy(); + boolean result = service.isHealthy(); - assertTrue(result); - } + assertTrue(result); + } - @Test - void testIsHealthy_ProvisionerDown_ReturnsFalse() throws MarketplaceException { - when(clientFactory.getAvailableInstances()).thenReturn(Set.of("dev")); - when(clientFactory.getDefaultInstanceName()).thenReturn("dev"); - when(clientFactory.getClient("dev")).thenReturn(marketplaceApiClient); + @Test + void testIsHealthy_ProvisionerDown_ReturnsFalse() throws MarketplaceException { + when(clientFactory.getAvailableInstances()).thenReturn(Set.of("dev")); + when(clientFactory.getDefaultInstanceName()).thenReturn("dev"); + when(clientFactory.getClient("dev")).thenReturn(marketplaceApiClient); - MarketplaceServiceImpl service = new MarketplaceServiceImpl(clientFactory, oboTokenService) { - @Override - protected boolean isProvisionerEndpointUp(MarketplaceApiClient marketplaceClient) { - return false; - } - }; + MarketplaceServiceImpl service = new MarketplaceServiceImpl(clientFactory, oboTokenService) { + @Override + protected boolean isProvisionerEndpointUp(MarketplaceApiClient marketplaceClient) { + return false; + } + }; - boolean result = service.isHealthy(); + boolean result = service.isHealthy(); - assertFalse(result); - } + assertFalse(result); + } - @Test - void testIsHealthy_CatalogDown_ReturnsFalse() throws MarketplaceException { - when(clientFactory.getAvailableInstances()).thenReturn(Set.of("dev")); - when(clientFactory.getDefaultInstanceName()).thenReturn("dev"); - when(clientFactory.getClient("dev")).thenReturn(marketplaceApiClient); + @Test + void testIsHealthy_CatalogDown_ReturnsFalse() throws MarketplaceException { + when(clientFactory.getAvailableInstances()).thenReturn(Set.of("dev")); + when(clientFactory.getDefaultInstanceName()).thenReturn("dev"); + when(clientFactory.getClient("dev")).thenReturn(marketplaceApiClient); - MarketplaceServiceImpl service = new MarketplaceServiceImpl(clientFactory, oboTokenService) { - @Override - protected boolean isProvisionerEndpointUp(MarketplaceApiClient marketplaceClient) { - return true; - } + MarketplaceServiceImpl service = new MarketplaceServiceImpl(clientFactory, oboTokenService) { + @Override + protected boolean isProvisionerEndpointUp(MarketplaceApiClient marketplaceClient) { + return true; + } - @Override - protected boolean isCatalogEndpointUp(MarketplaceApiClient marketplaceClient) { - return false; - } - }; + @Override + protected boolean isCatalogEndpointUp(MarketplaceApiClient marketplaceClient) { + return false; + } + }; - boolean result = service.isHealthy(); + boolean result = service.isHealthy(); - assertFalse(result); + assertFalse(result); } // ------------------------------------------------------------------------- @@ -447,11 +447,11 @@ void testProvisionProjectComponent_Conflict_ThrowsMarketplaceExceptionWithDuplic instanceConfig.setOboScope("api://test/scope"); HttpClientErrorException conflictEx = HttpClientErrorException.create( - HttpStatus.CONFLICT, - "Conflict", - HttpHeaders.EMPTY, - "{\"message\":\"This component name already exists, please choose another name.\"}".getBytes(StandardCharsets.UTF_8), - StandardCharsets.UTF_8 + HttpStatus.CONFLICT, + "Conflict", + HttpHeaders.EMPTY, + "{\"message\":\"This component name already exists, please choose another name.\"}".getBytes(StandardCharsets.UTF_8), + StandardCharsets.UTF_8 ); when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); @@ -460,7 +460,7 @@ void testProvisionProjectComponent_Conflict_ThrowsMarketplaceExceptionWithDuplic whenInvokeAPI(PATH_PROVISION_ACTIONS, HttpMethod.POST).thenThrow(conflictEx); MarketplaceException exception = assertThrows(MarketplaceException.class, () -> - marketplaceService.provisionProjectComponent(instanceName, projectKey, List.of())); + marketplaceService.provisionProjectComponent(instanceName, projectKey, List.of())); assertEquals("This component name already exists, please choose another name.", exception.getMessage()); } @@ -471,7 +471,7 @@ void testGetCatalogItem_RestClientException() throws MarketplaceException { String instanceName = "dev"; String catalogItemId = "test-catalog-item-base64-string"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setOboScope("api://test/scope"); + instanceConfig.setOboScope("api://test/scope"); when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); @@ -493,7 +493,7 @@ void testGetCatalogItem_NotFound_ReturnsNull() throws MarketplaceException { String instanceName = "dev"; String catalogItemId = "test-catalog-item-base64-string"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setOboScope("api://test/scope"); + instanceConfig.setOboScope("api://test/scope"); HttpClientErrorException notFoundEx = HttpClientErrorException.create( HttpStatus.NOT_FOUND, "Not Found", HttpHeaders.EMPTY, new byte[0], null); @@ -516,7 +516,7 @@ void testGetCatalogItem_AuthError_ThrowsMarketplaceException() throws Marketplac String instanceName = "dev"; String catalogItemId = "test-catalog-item-base64-string"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setOboScope("api://test/scope"); + instanceConfig.setOboScope("api://test/scope"); HttpClientErrorException forbiddenEx = HttpClientErrorException.create( HttpStatus.FORBIDDEN, "Forbidden", HttpHeaders.EMPTY, new byte[0], null); @@ -537,7 +537,7 @@ void testGetCatalogItem_DefaultInstance() throws MarketplaceException { // Arrange String catalogItemId = "test-catalog-item-base64-string"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setOboScope("api://test/scope"); + instanceConfig.setOboScope("api://test/scope"); HttpClientErrorException notFoundEx = HttpClientErrorException.create( HttpStatus.NOT_FOUND, "Not Found", HttpHeaders.EMPTY, new byte[0], null); @@ -730,132 +730,71 @@ void testProvisionProjectComponent_OboScopeNotConfigured_ThrowsMarketplaceExcept // ------------------------------------------------------------------------- // deleteProjectComponent // ------------------------------------------------------------------------- - @Test - void testDeleteProjectComponent_Success_ReturnsTrue() throws MarketplaceException { + void testDeleteProjectComponent_RestClientException() throws MarketplaceException { // Arrange String instanceName = "dev"; - String projectKey = "PROJ"; - String componentId = "test-component"; + String componentId = "test-component-id"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setProvisionerActionsBaseUrl("https://example/provision-actions"); - instanceConfig.setOboScope("api://test/scope"); - - ProvisionActionResponse mockResponse = new ProvisionActionResponse(); - mockResponse.setFailed(false); + when(clientFactory.getDefaultInstanceName()).thenReturn(instanceName); when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); - whenInvokeAPI(PATH_DELETE_COMPONENT, HttpMethod.POST) - .thenReturn(new ResponseEntity<>(mockResponse, HttpStatus.OK)); + when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenThrow(new RestClientException("Connection failed")); - // Act - boolean result = marketplaceService.deleteProjectComponent(instanceName, projectKey, componentId); + // Act & Assert + assertThrows(MarketplaceException.class, () -> + marketplaceService.deleteProjectComponent(instanceName, componentId)); - // Assert - assertTrue(result); verify(clientFactory).getClient(instanceName); + verify(marketplaceApiClient).getApiClient(); } @Test - void testDeleteProjectComponent_Failed_ReturnsFalse() throws MarketplaceException { + void testDeleteProjectComponent_Unauthorized_ThrowsException() throws MarketplaceException { // Arrange String instanceName = "dev"; - String projectKey = "PROJ"; - String componentId = "test-component"; + String componentId = "test-component-id"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setProvisionerActionsBaseUrl("https://example/provision-actions"); - instanceConfig.setOboScope("api://test/scope"); - - ProvisionActionResponse mockResponse = new ProvisionActionResponse(); - mockResponse.setFailed(true); + HttpClientErrorException unauthorizedException = HttpClientErrorException.create( + HttpStatus.UNAUTHORIZED, "Unauthorized", HttpHeaders.EMPTY, new byte[0], null); + when(clientFactory.getDefaultInstanceName()).thenReturn(instanceName); when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); + when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenThrow(unauthorizedException); when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); - whenInvokeAPI(PATH_DELETE_COMPONENT, HttpMethod.POST) - .thenReturn(new ResponseEntity<>(mockResponse, HttpStatus.OK)); // Act - boolean result = marketplaceService.deleteProjectComponent(instanceName, projectKey, componentId); + assertThrows(MarketplaceException.class, () -> + marketplaceService.deleteProjectComponent(instanceName, componentId)); // Assert - assertFalse(result); - } - - @Test - void testDeleteProjectComponent_RestClientException_ThrowsMarketplaceException() throws MarketplaceException { - // Arrange - String instanceName = "dev"; - String projectKey = "PROJ"; - String componentId = "test-component"; - MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setProvisionerActionsBaseUrl("https://example/provision-actions"); - instanceConfig.setOboScope("api://test/scope"); - - when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); - when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); - when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); - whenInvokeAPI(PATH_DELETE_COMPONENT, HttpMethod.POST) - .thenThrow(new RestClientException("Connection refused")); - - // Act & Assert - MarketplaceException exception = assertThrows(MarketplaceException.class, () -> - marketplaceService.deleteProjectComponent(instanceName, projectKey, componentId)); - - assertTrue(exception.getMessage().contains("Failed to delete")); + verify(clientFactory).getClient(instanceName); } @Test - void testDeleteProjectComponent_AuthError_ThrowsMarketplaceException() throws MarketplaceException { + void testDeleteProjectComponent_ComponentExists_NoExceptionThrown() throws MarketplaceException { // Arrange String instanceName = "dev"; - String projectKey = "PROJ"; - String componentId = "test-component"; + String componentId = "test-component-id"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setProvisionerActionsBaseUrl("https://example/provision-actions"); - instanceConfig.setOboScope("api://test/scope"); - HttpClientErrorException forbiddenEx = HttpClientErrorException.create( - HttpStatus.FORBIDDEN, "Forbidden", HttpHeaders.EMPTY, new byte[0], null); + when(clientFactory.getDefaultInstanceName()).thenReturn(instanceName); when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); + when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT).build()); when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); - whenInvokeAPI(PATH_DELETE_COMPONENT, HttpMethod.POST).thenThrow(forbiddenEx); - - // Act & Assert - MarketplaceException exception = assertThrows(MarketplaceException.class, () -> - marketplaceService.deleteProjectComponent(instanceName, projectKey, componentId)); - - assertTrue(exception.getMessage().contains("Access denied")); - } - - @Test - void testDeleteProjectComponent_DefaultInstance() throws MarketplaceException { - // Arrange - String projectKey = "PROJ"; - String componentId = "test-component"; - MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setProvisionerActionsBaseUrl("https://example/provision-actions"); - instanceConfig.setOboScope("api://test/scope"); - - ProvisionActionResponse mockResponse = new ProvisionActionResponse(); - mockResponse.setFailed(false); - - when(clientFactory.getDefaultInstanceName()).thenReturn("default"); - when(clientFactory.getClient("default")).thenReturn(marketplaceApiClient); - when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); - when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); - whenInvokeAPI(PATH_DELETE_COMPONENT, HttpMethod.POST) - .thenReturn(new ResponseEntity<>(mockResponse, HttpStatus.OK)); // Act - boolean result = marketplaceService.deleteProjectComponent(projectKey, componentId); + marketplaceService.deleteProjectComponent(instanceName, componentId); // Assert - assertTrue(result); - verify(clientFactory).getClient("default"); + verify(clientFactory).getClient(instanceName); } // -------------------------------------------------------------------------