diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsController.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsController.java index a78afeb..2f2e9c8 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsController.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsController.java @@ -28,9 +28,15 @@ public class ProjectComponentsController implements ProjectComponentsApi { @Override public ResponseEntity createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { - componentsFacade.provisionProjectComponent(projectId, createComponentRequest); - log.info("Created component '{}' for project '{}'", createComponentRequest.getName(), projectId); + if (Boolean.TRUE.equals(createComponentRequest.getRegisterOnly())) { + componentsFacade.registerProjectComponent(projectId, createComponentRequest); + log.info("Registered component '{}' for project '{}'", createComponentRequest.getName(), projectId); + } else { + componentsFacade.provisionProjectComponent(projectId, createComponentRequest); + log.info("Created component '{}' for project '{}'", createComponentRequest.getName(), projectId); + } + return componentResponseMapper.toResponseEntity( ComponentsResponseFactory.entityCreated(projectId, createComponentRequest.getName()) ); 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 5af8579..b00f230 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 @@ -10,6 +10,7 @@ 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.ComponentRegistrationException; import org.opendevstack.apiservice.project.exception.ComponentRetrievalException; import org.opendevstack.apiservice.project.model.CreateComponentResponse; import org.springframework.http.HttpStatus; @@ -142,6 +143,22 @@ public ResponseEntity handleComponentCreationException( return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); } + @ExceptionHandler(ComponentRegistrationException.class) + public ResponseEntity handleComponentRegisterException( + ComponentRegistrationException ex, + HttpServletRequest request) { + + log.error("Component registration failed: {}", ex.getMessage(), ex); + + CreateComponentResponse response = ComponentsResponseFactory.internalError( + request.getRequestURI(), + ex.getMessage(), + ComponentErrorKey.INTERNAL_ERROR + ); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + @ExceptionHandler(ComponentDeletionException.class) public ResponseEntity handleComponentDeletionException( ComponentDeletionException ex, diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentRegistrationException.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentRegistrationException.java new file mode 100644 index 0000000..8ae8245 --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentRegistrationException.java @@ -0,0 +1,12 @@ +package org.opendevstack.apiservice.project.exception; + +public class ComponentRegistrationException extends RuntimeException { + + public ComponentRegistrationException(String message) { + super(message); + } + + public ComponentRegistrationException(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 56cc6c3..01266c3 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 @@ -3,9 +3,10 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItem; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProjectComponentExtendedInfo; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisionActionParameter; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisioningStatusUpdateRequestAllOfParameters; import org.opendevstack.apiservice.externalservice.marketplace.service.CatalogItemOperations; import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; import org.opendevstack.apiservice.project.exception.CatalogItemNotFoundException; @@ -14,6 +15,7 @@ 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.ComponentRegistrationException; import org.opendevstack.apiservice.project.exception.ComponentRetrievalException; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; import org.opendevstack.apiservice.project.model.Component; @@ -175,13 +177,19 @@ private boolean isAccessDeniedCause(Throwable throwable) { return false; } - public boolean registerProjectComponent(String projectId, String componentId) { + public void registerProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { + String componentId = createComponentRequest.getName(); + String productId = createComponentRequest.getProductId(); try { - marketplaceExternalService.registerProjectComponent(projectId, componentId); - return true; + List parameters = marketplaceMapper + .mapCreateComponentRequestToRegisterComponentParameterList(createComponentRequest); + marketplaceExternalService.registerProjectComponent(projectId, componentId, productId, parameters); + log.info("Successfully registered component '{}' for project '{}'", componentId, projectId); } catch (MarketplaceException e) { - log.error("Failed to register component in marketplace for project with id {}", projectId, e); - return false; + log.error("Failed to register component '{}' for project '{}': {}", componentId, projectId, e.getMessage(), e); + throw new ComponentRegistrationException( + String.format("Failed to register component '%s' for project '%s': %s", componentId, projectId, e.getMessage()), e + ); } } } diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/MarketplaceMapper.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/MarketplaceMapper.java index 2a0eec4..19887f0 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/MarketplaceMapper.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/MarketplaceMapper.java @@ -2,11 +2,12 @@ import org.mapstruct.Mapper; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItemUserAction; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItemUserActionParameter; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItem; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItemUserAction; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItemUserActionParameter; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProjectComponentExtendedInfo; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisionActionParameter; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisioningStatusUpdateRequestAllOfParameters; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.CreateComponentRequest; import org.opendevstack.apiservice.project.model.EnvironmentsDTO; @@ -67,10 +68,40 @@ default List mapCreateComponentRequestToCreateComponen return parameters; } + default List mapCreateComponentRequestToRegisterComponentParameterList( + CreateComponentRequest createComponentRequest) { + if (createComponentRequest == null) { + return List.of(); + } + + List parameters = new ArrayList<>(); + if (createComponentRequest.getParams() != null && !createComponentRequest.getParams().isEmpty()) { + createComponentRequest.getParams().forEach((name, value) -> { + parameters.add(createRegisterParameter(name, value)); + }); + } + + return parameters; + } + default ProvisionActionParameter createParameter(String name, Object value, String type) { return new ProvisionActionParameter().name(name).type(type).value(value); } + default ProvisioningStatusUpdateRequestAllOfParameters createRegisterParameter(String name, Object value) { + List values; + if (value == null) { + values = List.of(""); + } else if (value instanceof List list) { + values = list.stream() + .map(Object::toString) + .toList(); + } else { + values = List.of(value.toString()); + } + return new ProvisioningStatusUpdateRequestAllOfParameters().name(name).values(values); + } + /** * Builds a map of parameter name -> type from the catalog item user actions. * The type comes from the {@link CatalogItemUserActionParameter#getType()} value 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 3f69e77..9c5b200 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 @@ -66,6 +66,25 @@ void create_project_component_returns_ok_with_component_name_in_path() { verify(componentsFacade).provisionProjectComponent(projectId, request); } + @Test + void create_project_component_with_register_only_returns_ok_with_component_name_in_path() { + String projectId = "testProjectId"; + CreateComponentRequest request = buildTestCreateComponentRequest(); // name = "testcomponent" + request.setRegisterOnly(Boolean.TRUE); + + doNothing().when(componentsFacade).registerProjectComponent(eq(projectId), eq(request)); + + ResponseEntity response = projectComponentsController.createProjectComponent(projectId, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getHttpStatus()).isEqualTo(HttpStatus.OK.name()); + assertThat(response.getBody().getErrorKey()).isEqualTo("000"); + assertThat(response.getBody().getMessage()).isEqualTo("Component created"); + assertThat(response.getBody().getPath()).isEqualTo("/api/pub/v0/projects/testProjectId/components/testcomponent"); + verify(componentsFacade).registerProjectComponent(projectId, request); + } + @Test void create_project_component_propagates_exception_when_facade_throws_exception() { String projectId = "testProjectId"; @@ -80,6 +99,38 @@ void create_project_component_propagates_exception_when_facade_throws_exception( verify(componentsFacade).provisionProjectComponent(projectId, request); } + @Test + void create_project_component_with_register_only_propagates_exception_when_facade_throws_exception() { + String projectId = "testProjectId"; + CreateComponentRequest request = buildTestCreateComponentRequest(); + request.setRegisterOnly(Boolean.TRUE); + + org.mockito.Mockito.doThrow(new RuntimeException("boom")) + .when(componentsFacade).registerProjectComponent(eq(projectId), eq(request)); + + assertThatThrownBy(() -> projectComponentsController.createProjectComponent(projectId, request)) + .isInstanceOf(RuntimeException.class) + .hasMessage("boom"); + verify(componentsFacade).registerProjectComponent(projectId, request); + } + + @Test + void create_project_component_with_register_only_false_delegates_to_provision_project_component() { + String projectId = "testProjectId"; + CreateComponentRequest request = buildTestCreateComponentRequest(); + request.setRegisterOnly(Boolean.FALSE); + + doNothing().when(componentsFacade).provisionProjectComponent(eq(projectId), any(CreateComponentRequest.class)); + + ResponseEntity response = projectComponentsController.createProjectComponent(projectId, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getMessage()).isEqualTo("Component created"); + assertThat(response.getBody().getPath()).isEqualTo("/api/pub/v0/projects/testProjectId/components/testcomponent"); + verify(componentsFacade).provisionProjectComponent(projectId, request); + } + @Test void get_project_component_returns_ok_when_component_exists() throws MarketplaceException { String projectId = "projectId"; diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandlerTest.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandlerTest.java index 92a0601..818fe30 100644 --- a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandlerTest.java +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandlerTest.java @@ -9,6 +9,7 @@ import org.opendevstack.apiservice.project.exception.ComponentAlreadyExistsException; import org.opendevstack.apiservice.project.exception.ComponentCreationException; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; +import org.opendevstack.apiservice.project.exception.ComponentRegistrationException; import org.opendevstack.apiservice.project.model.CreateComponentResponse; import org.springframework.core.MethodParameter; import org.springframework.http.HttpStatus; @@ -149,6 +150,20 @@ void handle_component_already_exists_exception_returns_conflict() { assertThat(response.getBody().getMessage()).isEqualTo("This component name already exists, please choose another name."); } + @Test + void handle_component_registration_exception_returns_internal_server_error() { + ComponentRegistrationException exception = new ComponentRegistrationException("Registration failed"); + + ResponseEntity response = handler.handleComponentRegisterException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getHttpStatus()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.name()); + assertThat(response.getBody().getErrorKey()).isEqualTo("003"); + assertThat(response.getBody().getMessage()).isEqualTo("Registration failed"); + assertThat(response.getBody().getPath()).isEqualTo("/api/pub/v0/projects/test-project/components/"); + } + @Test void handle_generic_exception_returns_internal_server_error() { RuntimeException exception = new RuntimeException("boom"); 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 0507499..370d784 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 @@ -7,13 +7,14 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItem; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProjectComponentExtendedInfo; 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.exception.ComponentRegistrationException; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.ComponentsStatusDTO; @@ -28,6 +29,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.opendevstack.apiservice.project.util.TestObjectsBuilder.buildTestCatalogItem; @@ -103,8 +105,8 @@ void create_project_component_throws_creation_exception_when_marketplace_returns verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), anyList()); } - @Test - void create_project_component_throws_already_exists_when_marketplace_returns_conflict() throws MarketplaceException { + @Test + void create_project_component_throws_already_exists_when_marketplace_returns_conflict() throws MarketplaceException { CreateComponentRequest request = buildTestCreateComponentRequest(); HttpClientErrorException conflict = HttpClientErrorException.Conflict.create( HttpStatus.CONFLICT, @@ -121,8 +123,52 @@ void create_project_component_throws_already_exists_when_marketplace_returns_con .isInstanceOf(ComponentAlreadyExistsException.class) .hasMessage("This component name already exists, please choose another name."); verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), anyList()); - } + } + + @Test + void register_project_component_ends_successfully_when_marketplace_registration_succeeds() throws MarketplaceException { + CreateComponentRequest request = buildTestCreateComponentRequest(); + String projectId = "testProjectId"; + + doNothing().when(marketplaceExternalService).registerProjectComponent(eq(projectId), anyString(), anyString(), anyList()); + + componentsFacade.registerProjectComponent(projectId, request); + + verify(marketplaceExternalService).registerProjectComponent(eq(projectId), anyString(), anyString(), anyList()); + } + + @Test + void register_project_component_passes_correct_component_id_and_product_id_to_marketplace() throws MarketplaceException { + CreateComponentRequest request = buildTestCreateComponentRequest(); + // name = "testcomponent", productId = "testProductId" + String projectId = "testProjectId"; + + doNothing().when(marketplaceExternalService).registerProjectComponent( + eq(projectId), eq("testcomponent"), eq("testProductId"), anyList()); + + componentsFacade.registerProjectComponent(projectId, request); + verify(marketplaceExternalService).registerProjectComponent( + eq(projectId), eq("testcomponent"), eq("testProductId"), anyList()); + } + + @Test + void register_project_component_throws_registration_exception_wrapping_original_cause() throws MarketplaceException { + CreateComponentRequest request = buildTestCreateComponentRequest(); + String projectId = "testProjectId"; + MarketplaceException originalCause = new MarketplaceException("downstream error", new RuntimeException("root")); + + doThrow(originalCause) + .when(marketplaceExternalService) + .registerProjectComponent(eq(projectId), anyString(), anyString(), anyList()); + + assertThatThrownBy(() -> componentsFacade.registerProjectComponent(projectId, request)) + .isInstanceOf(ComponentRegistrationException.class) + .hasMessage("Failed to register component 'testcomponent' for project 'testProjectId': downstream error") + .hasCause(originalCause); + + verify(marketplaceExternalService).registerProjectComponent(eq(projectId), anyString(), anyString(), anyList()); + } @Test void delete_project_component_ends_successfully_for_existing_component() throws MarketplaceException { diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/mapper/MarketplaceMapperTest.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/mapper/MarketplaceMapperTest.java index 6495d05..1adf2f3 100644 --- a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/mapper/MarketplaceMapperTest.java +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/mapper/MarketplaceMapperTest.java @@ -2,10 +2,11 @@ import org.junit.jupiter.api.Test; import org.mapstruct.factory.Mappers; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItemUserAction; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItemUserActionParameter; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItem; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItemUserAction; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItemUserActionParameter; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisionActionParameter; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisioningStatusUpdateRequestAllOfParameters; import org.opendevstack.apiservice.project.model.CreateComponentRequest; import java.util.List; @@ -87,6 +88,94 @@ void mapCreateComponentRequestToCreateComponentParameterList_returnsEmptyWhenReq assertThat(mapper.mapCreateComponentRequestToCreateComponentParameterList(null, null)).isEmpty(); } + // ------------------------------------------------------------------------- + // mapCreateComponentRequestToRegisterComponentParameterList + // ------------------------------------------------------------------------- + + @Test + void mapCreateComponentRequestToRegisterComponentParameterList_maps_string_values_correctly() { + CreateComponentRequest request = new CreateComponentRequest(); + request.setName("test-component"); + request.setProductId("some-slug"); + request.setParams(Map.of("my_param", "my_value")); + + List result = + mapper.mapCreateComponentRequestToRegisterComponentParameterList(request); + + assertThat(result).hasSize(1); + ProvisioningStatusUpdateRequestAllOfParameters param = + result.stream().filter(p -> "my_param".equals(p.getName())).findFirst().orElseThrow(); + assertThat(param.getValues()).containsExactly("my_value"); + } + + @Test + void mapCreateComponentRequestToRegisterComponentParameterList_maps_list_values_correctly() { + CreateComponentRequest request = new CreateComponentRequest(); + request.setName("test-component"); + request.setProductId("some-slug"); + List listValue = List.of("value1", "value2", "value3"); + request.setParams(Map.of("list_param", listValue)); + + List result = + mapper.mapCreateComponentRequestToRegisterComponentParameterList(request); + + assertThat(result).hasSize(1); + ProvisioningStatusUpdateRequestAllOfParameters param = + result.stream().filter(p -> "list_param".equals(p.getName())).findFirst().orElseThrow(); + assertThat(param.getValues()).containsExactlyInAnyOrder("value1", "value2", "value3"); + } + + @Test + void mapCreateComponentRequestToRegisterComponentParameterList_maps_non_string_value_to_string() { + CreateComponentRequest request = new CreateComponentRequest(); + request.setName("test-component"); + request.setProductId("some-slug"); + request.setParams(Map.of("bool_param", true, "int_param", 42)); + + List result = + mapper.mapCreateComponentRequestToRegisterComponentParameterList(request); + + assertThat(result).hasSize(2); + ProvisioningStatusUpdateRequestAllOfParameters boolParam = + result.stream().filter(p -> "bool_param".equals(p.getName())).findFirst().orElseThrow(); + assertThat(boolParam.getValues()).containsExactly("true"); + + ProvisioningStatusUpdateRequestAllOfParameters intParam = + result.stream().filter(p -> "int_param".equals(p.getName())).findFirst().orElseThrow(); + assertThat(intParam.getValues()).containsExactly("42"); + } + + @Test + void mapCreateComponentRequestToRegisterComponentParameterList_maps_null_value_to_empty_string_list() { + CreateComponentRequest request = new CreateComponentRequest(); + request.setName("test-component"); + request.setProductId("some-slug"); + java.util.Map params = new java.util.HashMap<>(); + params.put("null_param", null); + request.setParams(params); + + List result = + mapper.mapCreateComponentRequestToRegisterComponentParameterList(request); + + assertThat(result).hasSize(1); + ProvisioningStatusUpdateRequestAllOfParameters param = + result.stream().filter(p -> "null_param".equals(p.getName())).findFirst().orElseThrow(); + assertThat(param.getValues()).containsExactly(""); + } + + @Test + void mapCreateComponentRequestToRegisterComponentParameterList_returns_empty_list_when_params_null() { + CreateComponentRequest request = new CreateComponentRequest(); + request.setName("test-component"); + request.setProductId("some-slug"); + request.setParams(null); + + List result = + mapper.mapCreateComponentRequestToRegisterComponentParameterList(request); + + assertThat(result).isEmpty(); + } + private static CatalogItemUserAction buildProvisionAction(CatalogItemUserActionParameter... params) { CatalogItemUserAction action = new CatalogItemUserAction(); action.setId("PROVISION"); diff --git a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/util/TestObjectsBuilder.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/util/TestObjectsBuilder.java index a9c059f..b40d908 100644 --- a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/util/TestObjectsBuilder.java +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/util/TestObjectsBuilder.java @@ -1,7 +1,7 @@ package org.opendevstack.apiservice.project.util; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItem; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProjectComponentExtendedInfo; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.ComponentsStatusDTO; import org.opendevstack.apiservice.project.model.CreateComponentRequest; diff --git a/application.yaml b/application.yaml index deb7058..a334b88 100644 --- a/application.yaml +++ b/application.yaml @@ -302,5 +302,8 @@ externalservices: default: project-components-base-url: ${MARKETPLACE_PROJECT_COMPONENTS_BASE_URL} provisioner-actions-base-url: ${MARKETPLACE_PROVISIONER_ACTIONS_BASE_URL:} + bitbucket-base-url: ${MARKETPLACE_BITBUCKET_BASE_URL:} obo-scope: ${MARKETPLACE_OBO_SCOPE:} trust-all-certificates: ${MARKETPLACE_TRUST_ALL_CERTS:false} + username: ${MARKETPLACE_USERNAME:} + password: ${MARKETPLACE_PASSWORD:} 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 69e19ab..2cf2681 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 @@ -164,11 +164,13 @@ paths: /provision/{projectKey}/{status}: put: + security: + - basicAuth: [ ] # Enable ONLY basicAuth tags: - ProvisionResults summary: Notify provisioning Status Update description: > - This endpoint receives provisioning status update notifications from AWX. + This endpoint receives provisioning updates. operationId: notifyProvisioningStatusUpdate parameters: - name: projectKey @@ -190,24 +192,46 @@ paths: content: application/json: schema: - type: object - properties: - componentId: - type: string - description: The componentId set by the user. - example: "any-component-id-from-backend" - catalogItemId: - type: string - description: The base64 encoded path for the catalogItem. Mind that it may include branch reference. - example: "cHJvamVjdHMvQ0FURVNUL3JlcG9zL3VzZXItYWN0aW9ucy1pdGVtL3Jhdy9DYXRhbG9nSXRlbS55YW1sP2F0PXJlZnMvaGVhZHMvbWFzdGVy" - catalogItemSlug: - type: string - description: The slug for the provisioned component. - example: "myproject_repo_name" - componentUrl: - type: string - description: The bitbucket repository url for the provisioned component. - example: "https://bitbucket.com/projects/myproject/repos/repo_name" + $ref: '#/components/schemas/ProvisioningStatusUpdateRequest' + responses: + "200": + description: Provisioning completion notified. + "400": + description: Bad request. + "401": + description: Invalid client token on the request. + "403": + description: Insufficient permissions for the client to access the resource. + "500": + description: Server error. + patch: + tags: + - ProvisionResults + summary: Notify provisioning Status Update + description: > + This endpoint receives provisioning status update notifications from AAP. + operationId: notifyProvisioningStatusUpdatePartially + parameters: + - name: projectKey + in: path + description: Project key of the provisioned component. + required: true + schema: + type: string + - name: status + in: path + description: Project key of the provisioned component. + required: true + example: CREATING + schema: + type: string + enum: [ CREATING, CREATED, FAILED, DELETING, UNKNOWN ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProvisioningStatusPartialUpdateRequest' responses: "200": description: Provisioning completion notified. @@ -226,10 +250,10 @@ paths: - basicAuth: [ ] # Enable ONLY basicAuth tags: - ProvisionResults - summary: Delete provision status component from the file + summary: Delete project component from the file description: > - This endpoint receives provisioning status delete notifications from Component Provisioner. - operationId: deleteProvisioningStatus + This endpoint receives provisioning status delete notifications from Project Component. + operationId: deleteProjectComponent parameters: - name: projectKey in: path @@ -255,14 +279,14 @@ paths: "500": description: Server error. - /support/delete/{projectKey}/{componentId}: + /provision/delete/{projectKey}/{componentId}: post: tags: - ProvisionResults summary: Request App Support to do operations to delete provision status component (and dependencies) from the file description: > This endpoint receives project key and componentId and send an create an incident to app support. - operationId: createIncident + operationId: requestDeletion parameters: - name: projectKey in: path @@ -297,6 +321,51 @@ paths: description: Insufficient permissions for the client to access the resource. "500": description: Server error. + + /project/{projectKey}/component/{componentId}: + get: + tags: + - Project-components-with-provision-status + summary: Returns the provision status of a project component given both its project key and component ID. + operationId: getProjectComponentProvisionStatusById + parameters: + - name: projectKey + in: path + description: project key. + required: true + schema: + type: string + - name: componentId + in: path + description: component ID. + required: true + schema: + type: string + responses: + "200": + description: The provision status information of a project component, including fail reason if exists. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectComponentProvisionStatus' + "401": + description: Invalid client token on the request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "403": + description: Insufficient permissions for the client to access the resource. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' components: securitySchemes: bearerAuth: @@ -415,6 +484,61 @@ components: required: - message + ProvisioningStatusPartialUpdateRequest: + type: object + properties: + componentId: + type: string + minLength: 1 # disallows empty string "" + pattern: '^(?!\s*$).+' # reject whitespace-only + description: The componentId set by the user. + example: "any-component-id-from-backend" + catalogItemId: + type: string + description: The base64 encoded path for the catalogItem. It may include branch reference. + example: "cHJvamVjdHMvQ0FURVNUL3JlcG9zL3VzZXItYWN0aW9ucy1pdGVtL3Jhdy9DYXRhbG9nSXRlbS55YW1sP2F0PXJlZnMvaGVhZHMvbWFzdGVy" + catalogItemSlug: + type: string + description: The slug for the provisioned component. + example: "myproject_repo_name" + componentUrl: + type: string + description: the repository url where the component was provisioned + example: "https://bitbucket.com/projects/DEVSTACK/repos/devstack-component-catalog" + nullable: true + + ProvisioningStatusUpdateRequest: + allOf: + - $ref: '#/components/schemas/ProvisioningStatusPartialUpdateRequest' + - type: object + properties: + workflowJobId: + type: string + description: the workflow job id from AWX to correlate provisioning status with AWX job status updates + example: "123456" + nullable: true + parameters: + type: array + description: List of name/value string parameters. + items: + type: object + required: + - name + - values + properties: + name: + type: string + description: Parameter name + example: "environment" + values: + type: array + description: Parameter values + items: + type: string + example: + - "production" + - "staging" + ProvisioningDeleteRequest: type: object properties: @@ -440,4 +564,50 @@ components: example: name: "workflow" type: "string" - value: "2558" \ No newline at end of file + value: "2558" + + ProjectComponentStatusParameter: + properties: + name: + type: string + example: 'environment' + values: + type: array + items: + type: string + example: + - 'dev' + - 'test' + ProjectComponentProvisionStatus: + properties: + projectKey: + type: string + example: 'simple-project-sample' + componentId: + type: string + example: 'nextjs-basic-app' + catalogItemId: + type: string + example: 'some-encoded-info' + catalogItemRef: + type: string + example: 'more-encoded-info' + status: + type: string + example: 'CREATING' + componentUrl: + type: string + example: 'https://bitbucket.com/projects/CATALOGS/repos/project-components/browse/projects' + workflowJobId: + type: string + example: '1316315' + errorTask: + type: string + example: '08-01-fail if validations or checks did not pass' + errorMessage: + type: string + example: 'JIRA_ERROR' + parameters: + type: array + items: + $ref: '#/components/schemas/ProjectComponentStatusParameter' \ No newline at end of file diff --git a/external-service-marketplace/pom.xml b/external-service-marketplace/pom.xml index a8799c6..061eceb 100644 --- a/external-service-marketplace/pom.xml +++ b/external-service-marketplace/pom.xml @@ -152,12 +152,12 @@ FILTER=operationId:getProjectComponents|getProjectComponentById|getCatalogItemById|getCatalogItemBySlug|getCatalogHealth java - ${project.basedir} + ${project.basedir}/target/generated-sources/openapi resttemplate ${project.basedir}/openapi/openapi-component_catalog-v1.0.0.yaml - org.opendevstack.apiservice.externalservice.marketplace.openapi.api - org.opendevstack.apiservice.externalservice.marketplace.openapi.model - org.opendevstack.apiservice.externalservice.marketplace.openapi + org.opendevstack.apiservice.externalservice.marketplace.client.api + org.opendevstack.apiservice.externalservice.marketplace.client.model + org.opendevstack.apiservice.externalservice.marketplace.client false true true @@ -186,14 +186,14 @@ generate - FILTER=operationId:triggerProvisionAction|notifyProvisioningStatusUpdate|deleteProvisioningStatus|getProvisionerHealth + FILTER=operationId:triggerProvisionAction|notifyProvisioningStatusUpdate|deleteProjectComponent|getProvisionerHealth java - ${project.basedir} + ${project.basedir}/target/generated-sources/openapi resttemplate ${project.basedir}/openapi/openapi-component_provisioner-v1.0.0.yaml - org.opendevstack.apiservice.externalservice.marketplace.openapi.api - org.opendevstack.apiservice.externalservice.marketplace.openapi.model - org.opendevstack.apiservice.externalservice.marketplace.openapi + org.opendevstack.apiservice.externalservice.marketplace.client.api + org.opendevstack.apiservice.externalservice.marketplace.client.model + org.opendevstack.apiservice.externalservice.marketplace.client false true true 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 53d2f04..6443f56 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 @@ -6,8 +6,8 @@ import org.openapitools.jackson.nullable.JsonNullableModule; import org.opendevstack.apiservice.core.security.util.Base64Operations; import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceInstanceConfig; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.ApiClient; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.auth.HttpBearerAuth; +import org.opendevstack.apiservice.externalservice.marketplace.client.ApiClient; +import org.opendevstack.apiservice.externalservice.marketplace.client.auth.HttpBearerAuth; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.client.RestTemplate; 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 9fb66a1..c0645b0 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,11 @@ public class MarketplaceInstanceConfig { */ private String provisionerActionsBaseUrl; + /** + * The Bitbucket base URL of the Marketplace project + */ + private String bitbucketBaseUrl; + /** * The username used for basic auth */ diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/CatalogItemOperations.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/CatalogItemOperations.java index 14919d4..f962ed8 100644 --- a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/CatalogItemOperations.java +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/CatalogItemOperations.java @@ -1,6 +1,6 @@ package org.opendevstack.apiservice.externalservice.marketplace.service; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProjectComponentExtendedInfo; import java.nio.charset.StandardCharsets; import java.util.Base64; 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 29ffdf4..30155e8 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 @@ -2,9 +2,10 @@ import org.opendevstack.apiservice.externalservice.api.ExternalService; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItem; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProjectComponentExtendedInfo; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisionActionParameter; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisioningStatusUpdateRequestAllOfParameters; import java.util.List; import java.util.Set; @@ -37,8 +38,8 @@ public interface MarketplaceService extends ExternalService { void deleteProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException; - void registerProjectComponent(String projectId, String componentId) throws MarketplaceException; + void registerProjectComponent(String projectId, String componentId, String catalogItemSlug, List params) throws MarketplaceException; - void registerProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException; + void registerProjectComponent(String instanceName, String projectId, String componentId, String catalogItemSlug, List params) 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 de7e36e..c72bcea 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 @@ -7,22 +7,23 @@ import org.opendevstack.apiservice.externalservice.marketplace.client.MarketplaceApiClientFactory; import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceInstanceConfig; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.ApiClient; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.api.CatalogHealthApi; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.api.CatalogItemsApi; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.api.ProjectComponentsApi; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.api.ProvisionResultsApi; -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.GetCatalogHealth200Response; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.GetProvisionerHealth200Response; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.NotifyProvisioningStatusUpdateRequest; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; -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.client.ApiClient; +import org.opendevstack.apiservice.externalservice.marketplace.client.api.CatalogHealthApi; +import org.opendevstack.apiservice.externalservice.marketplace.client.api.CatalogItemsApi; +import org.opendevstack.apiservice.externalservice.marketplace.client.api.ProjectComponentsApi; +import org.opendevstack.apiservice.externalservice.marketplace.client.api.ProvisionResultsApi; +import org.opendevstack.apiservice.externalservice.marketplace.client.api.ProvisionerActionsApi; +import org.opendevstack.apiservice.externalservice.marketplace.client.api.ProvisionerHealthApi; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItem; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.GetCatalogHealth200Response; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.GetProvisionerHealth200Response; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProjectComponentExtendedInfo; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisionAction; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisionActionParameter; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisionActionResponse; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisioningDeleteRequest; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisioningStatusUpdateRequest; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisioningStatusUpdateRequestAllOfParameters; import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; @@ -194,7 +195,7 @@ public void deleteProjectComponent(String instanceName, String projectId, String ProvisioningDeleteRequest provisioningDeleteRequest = new ProvisioningDeleteRequest(); provisioningDeleteRequest.setComponentId(componentId); - provisionResultsApi.deleteProvisioningStatus(projectId, provisioningDeleteRequest); + provisionResultsApi.deleteProjectComponent(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'", @@ -207,23 +208,36 @@ public void deleteProjectComponent(String instanceName, String projectId, String } @Override - public void registerProjectComponent(String projectId, String componentId) throws MarketplaceException { - registerProjectComponent(getDefaultInstance(), projectId, componentId); + public void registerProjectComponent(String projectId, + String componentId, + String catalogItemSlug, + List params) + throws MarketplaceException { + registerProjectComponent(getDefaultInstance(), projectId, componentId, catalogItemSlug, params); } @Override - public void registerProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException { + public void registerProjectComponent(String instanceName, + String projectId, + String componentId, + String catalogItemSlug, + List params) + throws MarketplaceException { log.debug("Marketplace service REGISTER 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()); - NotifyProvisioningStatusUpdateRequest registerRequest = new NotifyProvisioningStatusUpdateRequest(); - registerRequest.setComponentId(componentId); + ProvisioningStatusUpdateRequest registerRequest = new ProvisioningStatusUpdateRequest() + .componentId(componentId) + .catalogItemSlug(catalogItemSlug) + .componentUrl(String.format("%s/projects/%s/repos/%s/browse", + marketplaceClient.getConfig().getBitbucketBaseUrl(), projectId, componentId)) + .parameters(params); provisionResultsApi.notifyProvisioningStatusUpdate(projectId, "CREATED", registerRequest); } catch (HttpClientErrorException.Unauthorized | HttpClientErrorException.Forbidden e) { throw new MarketplaceException( diff --git a/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/service/CatalogItemOperationsTest.java b/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/service/CatalogItemOperationsTest.java index 74bce9f..c019dbf 100644 --- a/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/service/CatalogItemOperationsTest.java +++ b/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/service/CatalogItemOperationsTest.java @@ -1,7 +1,7 @@ package org.opendevstack.apiservice.externalservice.marketplace.service; import org.junit.jupiter.api.Test; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProjectComponentExtendedInfo; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; 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 03fb26e..6ac9782 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 @@ -4,18 +4,20 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.OngoingStubbing; import org.opendevstack.apiservice.core.security.obo.OboTokenService; import org.opendevstack.apiservice.externalservice.marketplace.client.MarketplaceApiClient; import org.opendevstack.apiservice.externalservice.marketplace.client.MarketplaceApiClientFactory; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisioningStatusUpdateRequest; import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceInstanceConfig; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.ApiClient; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionResponse; +import org.opendevstack.apiservice.externalservice.marketplace.client.ApiClient; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItem; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProjectComponentExtendedInfo; +import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisionActionResponse; import org.opendevstack.apiservice.externalservice.marketplace.service.impl.MarketplaceServiceImpl; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; @@ -802,94 +804,113 @@ void testDeleteProjectComponent_ComponentExists_NoExceptionThrown() throws Marke // ------------------------------------------------------------------------- @Test - void testRegisterProjectComponent_Success() throws MarketplaceException { + void testRegisterProjectComponent_RestClientException() throws MarketplaceException { // Arrange String instanceName = "dev"; String projectKey = "PROJ"; - String componentId = "test-component"; + String componentId = "test-component-id"; + String catalogItemSlug = "test-catalog-item-slug"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setProvisionerActionsBaseUrl("https://example/provision-actions"); - instanceConfig.setOboScope("api://test/scope"); + when(clientFactory.getDefaultInstanceName()).thenReturn(instanceName); when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); - whenInvokeAPI(PATH_NOTIFY_PROVISIONING, HttpMethod.PUT).thenReturn(ResponseEntity.ok(null)); + whenInvokeAPI(PATH_NOTIFY_PROVISIONING, HttpMethod.PUT).thenThrow(new RestClientException("Connection failed")); // Act — should not throw - marketplaceService.registerProjectComponent(instanceName, projectKey, componentId); + assertThrows(MarketplaceException.class, () -> + marketplaceService.registerProjectComponent(projectKey, componentId, catalogItemSlug, List.of())); // Assert verify(clientFactory).getClient(instanceName); + verify(marketplaceApiClient).getApiClient(); } @Test - void testRegisterProjectComponent_RestClientException_ThrowsMarketplaceException() throws MarketplaceException { + void testRegisterProjectComponent_Unauthorized_ThrowsException() throws MarketplaceException { // Arrange String instanceName = "dev"; String projectKey = "PROJ"; - String componentId = "test-component"; + String componentId = "test-component-id"; + String catalogItemSlug = "test-catalog-item-slug"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setProvisionerActionsBaseUrl("https://example/provision-actions"); - instanceConfig.setOboScope("api://test/scope"); + 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(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); - whenInvokeAPI(PATH_NOTIFY_PROVISIONING, HttpMethod.PUT) - .thenThrow(new RestClientException("Connection refused")); + whenInvokeAPI(PATH_NOTIFY_PROVISIONING, HttpMethod.PUT).thenThrow(unauthorizedException); // Act & Assert - MarketplaceException exception = assertThrows(MarketplaceException.class, () -> - marketplaceService.registerProjectComponent(instanceName, projectKey, componentId)); + assertThrows(MarketplaceException.class, () -> + marketplaceService.registerProjectComponent(projectKey, componentId, catalogItemSlug, List.of())); - assertTrue(exception.getMessage().contains("Failed to register")); + verify(clientFactory).getClient(instanceName); } @Test - void testRegisterProjectComponent_AuthError_ThrowsMarketplaceException() throws MarketplaceException { + void testRegisterProjectComponent_BuildsCorrectComponentUrl_UsingBitbucketBaseUrl() throws MarketplaceException { // Arrange String instanceName = "dev"; String projectKey = "PROJ"; - String componentId = "test-component"; + String componentId = "my-component"; + String catalogItemSlug = "test-slug"; + String bitbucketBaseUrl = "https://bitbucket.example.com"; + MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setProvisionerActionsBaseUrl("https://example/provision-actions"); - instanceConfig.setOboScope("api://test/scope"); - HttpClientErrorException unauthorizedEx = HttpClientErrorException.create( - HttpStatus.UNAUTHORIZED, "Unauthorized", HttpHeaders.EMPTY, new byte[0], null); + instanceConfig.setBitbucketBaseUrl(bitbucketBaseUrl); + + ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(Object.class); + when(clientFactory.getDefaultInstanceName()).thenReturn(instanceName); when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); - whenInvokeAPI(PATH_NOTIFY_PROVISIONING, HttpMethod.PUT).thenThrow(unauthorizedEx); + whenInvokeAPI(PATH_NOTIFY_PROVISIONING, HttpMethod.PUT).thenReturn(ResponseEntity.ok(null)); - // Act & Assert - MarketplaceException exception = assertThrows(MarketplaceException.class, () -> - marketplaceService.registerProjectComponent(instanceName, projectKey, componentId)); + // Act + marketplaceService.registerProjectComponent(projectKey, componentId, catalogItemSlug, List.of()); - assertTrue(exception.getMessage().contains("Access denied")); + // Assert — capture the body passed to invokeAPI and verify its componentUrl + verify(apiClient).invokeAPI( + eq(PATH_NOTIFY_PROVISIONING), + eq(HttpMethod.PUT), + any(), + any(), + bodyCaptor.capture(), + any(HttpHeaders.class), + any(), any(), any(), any(), any(), any()); + + ProvisioningStatusUpdateRequest capturedRequest = + (ProvisioningStatusUpdateRequest) bodyCaptor.getValue(); + + String expectedUrl = bitbucketBaseUrl + "/projects/" + projectKey + "/repos/" + componentId + "/browse"; + assertEquals(expectedUrl, capturedRequest.getComponentUrl()); } @Test - void testRegisterProjectComponent_DefaultInstance() throws MarketplaceException { + void testRegisterProjectComponent_ComponentIsRegistered_NoExceptionThrown() throws MarketplaceException { // Arrange + String instanceName = "dev"; String projectKey = "PROJ"; String componentId = "test-component"; + String catalogItemSlug = "test-catalog-item-slug"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setProvisionerActionsBaseUrl("https://example/provision-actions"); - instanceConfig.setOboScope("api://test/scope"); - when(clientFactory.getDefaultInstanceName()).thenReturn("default"); - when(clientFactory.getClient("default")).thenReturn(marketplaceApiClient); + when(clientFactory.getDefaultInstanceName()).thenReturn(instanceName); + when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); whenInvokeAPI(PATH_NOTIFY_PROVISIONING, HttpMethod.PUT).thenReturn(ResponseEntity.ok(null)); - // Act — should not throw - marketplaceService.registerProjectComponent(projectKey, componentId); + // Act + marketplaceService.registerProjectComponent(projectKey, componentId, catalogItemSlug, List.of()); // Assert - verify(clientFactory).getClient("default"); + verify(clientFactory).getClient(instanceName); } } \ No newline at end of file