From 636cc1c4fd59a44673c2391f272bde32e36cd208 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Tue, 14 Apr 2026 15:50:45 +0200 Subject: [PATCH 01/25] Add marketplace api client. --- .gitignore | 2 + .../controller/ComponentsResponseFactory.java | 4 +- .../ProjectComponentsController.java | 8 +- .../project/facade/ComponentsFacade.java | 20 +- ...MarketplaceExternalServicePlaceholder.java | 27 - .../project/mapper/MarketplaceMapper.java | 10 +- .../ProjectComponentsControllerTest.java | 7 +- .../project/facade/ComponentsFacadeTest.java | 23 +- .../project/util/TestObjectsBuilder.java | 20 +- .../.openapi-generator-ignore | 43 + .../openapi-component_catalog-v1.0.0.yaml | 1212 +++++++++++++++++ .../openapi-component_provisioner-v1.0.0.yaml | 411 ++++++ external-service-marketplace/pom.xml | 122 ++ .../client/MarketplaceApiClient.java | 73 + .../client/MarketplaceApiClientFactory.java | 204 +++ .../config/MarketplaceServiceConfig.java | 85 ++ .../exception/MarketplaceClientException.java | 12 + .../model/CreateComponentParameter.java | 17 - .../marketplace/model/ProjectComponent.java | 18 - .../service/MarketplaceService.java | 22 +- .../service/impl/MarketplaceServiceImpl.java | 131 ++ .../impl/MarketplaceServiceMockImpl.java | 74 - 22 files changed, 2358 insertions(+), 187 deletions(-) delete mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/facade/MarketplaceExternalServicePlaceholder.java create mode 100644 external-service-marketplace/.openapi-generator-ignore create mode 100644 external-service-marketplace/openapi/openapi-component_catalog-v1.0.0.yaml create mode 100644 external-service-marketplace/openapi/openapi-component_provisioner-v1.0.0.yaml create mode 100644 external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClient.java create mode 100644 external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClientFactory.java create mode 100644 external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceServiceConfig.java create mode 100644 external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/exception/MarketplaceClientException.java delete mode 100644 external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/model/CreateComponentParameter.java delete mode 100644 external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/model/ProjectComponent.java create mode 100644 external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/impl/MarketplaceServiceImpl.java delete mode 100644 external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/impl/MarketplaceServiceMockImpl.java diff --git a/.gitignore b/.gitignore index c765529..46a7ede 100644 --- a/.gitignore +++ b/.gitignore @@ -97,5 +97,7 @@ api-project/src/main/java/org/opendevstack/apiservice/project/api api-project/src/main/java/org/opendevstack/apiservice/project/model api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/api api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/model +external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/openapi + **/.openapi-generator diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java index c3f876a..eefba91 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java @@ -15,10 +15,10 @@ public static CreateComponentResponse error(String projectId) { return response; } - public static CreateComponentResponse entityCreated(String projectId, String componentId) { + public static CreateComponentResponse entityCreated(String projectId) { CreateComponentResponse response = new CreateComponentResponse(); response.setErrorCode(HttpStatus.CREATED.value()); - response.setMessage(componentId + " component created successfully in project " + projectId); + response.setMessage("Component created successfully in project " + projectId); return response; } } 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 f8b0ae8..4583364 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 @@ -26,13 +26,13 @@ public class ProjectComponentsController implements ProjectComponentsApi { @Override public ResponseEntity createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { try { - Component component = componentsFacade.createProjectComponent(projectId, createComponentRequest); - log.info("Created component {} for project id {} and request {}", component, projectId, createComponentRequest); - if (component == null) { + boolean success = componentsFacade.createProjectComponent(projectId, createComponentRequest); + log.info("Created component for project id {} and request {}", projectId, createComponentRequest); + if (!success) { log.error("Failed to create component for project '{}'", projectId); return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.error(projectId)); } - return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.entityCreated(projectId, component.getId())); + return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.entityCreated(projectId)); } catch (Exception e) { log.error("Error while trying to create component for project '" + projectId + "': " + e.getMessage(), e); return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.error(projectId)); 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 f36476f..8bfd854 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 @@ -2,9 +2,10 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.opendevstack.apiservice.externalservice.marketplace.model.CreateComponentParameter; -import org.opendevstack.apiservice.externalservice.marketplace.model.ProjectComponent; import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; +import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceClientException; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.CreateComponentRequest; @@ -21,8 +22,8 @@ public class ComponentsFacade { private final MarketplaceMapper marketplaceMapper; - public Component getProjectComponent(String projectId, String componentId) { - ProjectComponent marketplaceComponent = marketplaceExternalService.getProjectComponent(projectId, componentId); + public Component getProjectComponent(String projectId, String componentId) throws MarketplaceClientException { + ProjectComponentInfo marketplaceComponent = marketplaceExternalService.getProjectComponent(projectId, componentId); if (marketplaceComponent == null) { log.info("Marketplace component with id {} not found", componentId); return null; @@ -30,13 +31,12 @@ public Component getProjectComponent(String projectId, String componentId) { return marketplaceMapper.mapMarketplaceComponentToV0Component(marketplaceComponent); } - public Component createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { - List createComponentParameterList = marketplaceMapper.mapCreateComponentRequestToCreateComponentParameterList(createComponentRequest); - ProjectComponent marketplaceComponent = marketplaceExternalService.createProjectComponent(projectId, createComponentParameterList); - if (marketplaceComponent == null) { + public boolean createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) throws MarketplaceClientException { + List createComponentParameterList = marketplaceMapper.mapCreateComponentRequestToCreateComponentParameterList(createComponentRequest); + boolean success = marketplaceExternalService.provisionProjectComponent(projectId, createComponentParameterList); + if (!success) { log.error("Failed to create component in marketplace for project with id {}", projectId); - return null; } - return marketplaceMapper.mapMarketplaceComponentToV0Component(marketplaceComponent); + return success; } } diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/facade/MarketplaceExternalServicePlaceholder.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/facade/MarketplaceExternalServicePlaceholder.java deleted file mode 100644 index 990374c..0000000 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/facade/MarketplaceExternalServicePlaceholder.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.opendevstack.apiservice.project.facade; - -import lombok.extern.slf4j.Slf4j; -import org.opendevstack.apiservice.externalservice.api.ExternalService; -import org.opendevstack.apiservice.project.model.Component; -import org.opendevstack.apiservice.project.model.CreateComponentRequest; -import org.springframework.stereotype.Service; - -@Service -@Slf4j -class MarketplaceExternalServicePlaceholder implements ExternalService { - - @Override - public boolean isHealthy() { - return false; - } - - public Component getProjectComponent(String projectId, String componentId) { - log.info("Get component with id '" + componentId + "' for project '" + projectId + "'"); - return null; - } - - public Component createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { - log.info("Creating component for project '" + projectId + "'" + " with request: " + createComponentRequest); - return null; - } -} 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 f142064..95ed48c 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,8 +2,8 @@ import org.mapstruct.Mapper; -import org.opendevstack.apiservice.externalservice.marketplace.model.CreateComponentParameter; -import org.opendevstack.apiservice.externalservice.marketplace.model.ProjectComponent; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.CreateComponentRequest; @@ -12,7 +12,7 @@ @Mapper(componentModel = "spring") public interface MarketplaceMapper { - default Component mapMarketplaceComponentToV0Component(ProjectComponent source) { + default Component mapMarketplaceComponentToV0Component(ProjectComponentInfo source) { if (source == null) { return null; } @@ -22,9 +22,9 @@ default Component mapMarketplaceComponentToV0Component(ProjectComponent source) return target; } - default List mapCreateComponentRequestToCreateComponentParameterList(CreateComponentRequest createComponentRequest) { + default List mapCreateComponentRequestToCreateComponentParameterList(CreateComponentRequest createComponentRequest) { return createComponentRequest.getParams().entrySet().stream() - .map(entry -> new CreateComponentParameter(entry.getKey(), "string", entry.getValue())) + .map(entry -> new ProvisionActionParameter().name(entry.getKey()).type("string").value(entry.getValue())) .toList(); } } 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 a98c778..8b64e32 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 @@ -40,11 +40,10 @@ void testCreateProjectComponent_whenSuccess_thenReturnOk() throws Exception { Component testComponent = buildTestComponent(); String testProjectId = "testProjectId"; CreateComponentRequest testCreateComponentRequest = buildTestCreateComponentRequest(); - CreateComponentResponse testServiceResponseSuccess = buildTestCreateComponentResponseSuccess(testComponent.getId(), - testProjectId); + CreateComponentResponse testServiceResponseSuccess = buildTestCreateComponentResponseSuccess(testProjectId); when(componentsFacade.createProjectComponent(anyString(), any(CreateComponentRequest.class))) - .thenReturn(testComponent); + .thenReturn(true); ResponseEntity response = projectComponentsController.createProjectComponent(testProjectId, testCreateComponentRequest); @@ -61,7 +60,7 @@ void testCreateProjectComponent_whenFailure_thenReturnErrorResponse() throws Exc CreateComponentResponse testServiceResponseFailure = buildTestCreateComponentResponseFailure(testProjectId); when(componentsFacade.createProjectComponent(anyString(), any(CreateComponentRequest.class))) - .thenReturn(null); + .thenReturn(false); ResponseEntity response = projectComponentsController.createProjectComponent(testProjectId, testCreateComponentRequest); 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 3935839..4231ff7 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 @@ -6,8 +6,8 @@ import org.mapstruct.factory.Mappers; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.opendevstack.apiservice.externalservice.marketplace.model.ProjectComponent; import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.CreateComponentRequest; @@ -39,7 +39,7 @@ void setup() { @Test void testGetProjectComponent_whenSuccess_thenReturnCorrectComponent() throws Exception { - ProjectComponent testComponent = buildTestMarketplaceComponent(); + ProjectComponentInfo testComponent = buildTestMarketplaceComponent(); when(marketplaceExternalService.getProjectComponent(anyString(), eq("testId"))) .thenReturn(testComponent); @@ -60,15 +60,14 @@ void testGetProjectComponent_whenNoComponentFound_thenReturnNull() throws Except @Test void testCreateProjectComponent_whenSuccess_thenReturnCorrectComponent() throws Exception { - ProjectComponent testComponent = buildTestMarketplaceComponent(); + ProjectComponentInfo testComponent = buildTestMarketplaceComponent(); CreateComponentRequest testRequest = buildTestCreateComponentRequest(); - when(marketplaceExternalService.createProjectComponent(anyString(), any(List.class))) - .thenReturn(testComponent); + when(marketplaceExternalService.provisionProjectComponent(anyString(), any(List.class))) + .thenReturn(true); - Component retrievedComponent = componentsFacade.createProjectComponent("testId", testRequest); - assertThat(retrievedComponent.getId()).isEqualTo(testComponent.getComponentId()); - assertThat(retrievedComponent.getStatus()).isEqualTo(testComponent.getStatus()); + boolean result = componentsFacade.createProjectComponent("testId", testRequest); + assertThat(result).isTrue(); } @@ -76,10 +75,10 @@ void testCreateProjectComponent_whenSuccess_thenReturnCorrectComponent() throws void testCreateProjectComponent_whenFailure_thenReturnNull() throws Exception { CreateComponentRequest testRequest = buildTestCreateComponentRequest(); - when(marketplaceExternalService.createProjectComponent(anyString(), any(List.class))) - .thenReturn(null); + when(marketplaceExternalService.provisionProjectComponent(anyString(), any(List.class))) + .thenReturn(false); - Component retrievedComponent = componentsFacade.createProjectComponent("testId", testRequest); - assertThat(retrievedComponent).isNull(); + boolean result = componentsFacade.createProjectComponent("testId", testRequest); + assertThat(result).isFalse(); } } \ No newline at end of file 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 14cc84f..74b6e15 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.model.CreateComponentParameter; -import org.opendevstack.apiservice.externalservice.marketplace.model.ProjectComponent; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.CreateComponentRequest; import org.opendevstack.apiservice.project.model.CreateComponentResponse; @@ -24,8 +24,8 @@ public static Component buildTestComponent() { return component; } - public static ProjectComponent buildTestMarketplaceComponent() { - ProjectComponent component = new ProjectComponent(); + public static ProjectComponentInfo buildTestMarketplaceComponent() { + ProjectComponentInfo component = new ProjectComponentInfo(); component.setComponentId("testComponentId"); component.setCanBeDeleted(false); component.setComponentUrl("http://test.component.url"); @@ -39,17 +39,17 @@ public static CreateComponentRequest buildTestCreateComponentRequest() { return request; } - public static List buildTestMarketplaceCreateComponentParameters() { - List parameters = new ArrayList<>(); - parameters.add(new CreateComponentParameter("name", "string", "testComponentName")); - parameters.add(new CreateComponentParameter("productId", "string", "testProductId")); + public static List buildTestMarketplaceCreateComponentParameters() { + List parameters = new ArrayList<>(); + parameters.add(new ProvisionActionParameter().name("name").type("string").value("testComponentName")); + parameters.add(new ProvisionActionParameter().name("productId").type("string").value("testProductId")); return parameters; } - public static CreateComponentResponse buildTestCreateComponentResponseSuccess(String componentId, String projectId) { + public static CreateComponentResponse buildTestCreateComponentResponseSuccess(String projectId) { CreateComponentResponse response = new CreateComponentResponse(); response.setErrorCode(HttpStatus.CREATED.value()); - response.setMessage(componentId + " component created successfully in project " + projectId); + response.setMessage("Component created successfully in project " + projectId); return response; } diff --git a/external-service-marketplace/.openapi-generator-ignore b/external-service-marketplace/.openapi-generator-ignore new file mode 100644 index 0000000..82ac4f7 --- /dev/null +++ b/external-service-marketplace/.openapi-generator-ignore @@ -0,0 +1,43 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md + +api +api/** +gradle +gradle/** +.github +.github/** +pom.xml +**/AndroidManifest.xml +.gitignore +.openapi-generator-ignore +.travis.yml +build.gradle +build.sbt +git_push.sh +gradle.properties +gradlew +gradlew.bat +settings.gradle + diff --git a/external-service-marketplace/openapi/openapi-component_catalog-v1.0.0.yaml b/external-service-marketplace/openapi/openapi-component_catalog-v1.0.0.yaml new file mode 100644 index 0000000..bf4b70c --- /dev/null +++ b/external-service-marketplace/openapi/openapi-component_catalog-v1.0.0.yaml @@ -0,0 +1,1212 @@ +openapi: 3.0.3 +info: + title: Component Catalog REST API + version: '1.0.0' + description: > + The Component Catalog API allows clients to retrieve information about CatalogItems, CatalogFilters and Files entities. + + Catalog and File entities also exist internally, but only referenced by id's on requests and not returned on responses. + + **NOTES**: + - The OpenAPI specification file is also used to [generate](https://openapi-generator.tech/) REST client(s) and a server REST API. + - Clients and servers generated from the same OpenAPI specification version are guaranteed to be **compatible**. + contact: + name: EDPCore Team + url: https://confluence.biscrum.com/pages/viewpage.action?spaceKey=EDP&title=Welcome +servers: + - url: http://{baseurl}/v1 + variables: + baseurl: + default: localhost:8080 + description: Default address for a Component Catalog's backend REST API instance. +security: + - bearerAuth: [ ] +tags: + - name: CatalogItems + description: CatalogItems operations. + - name: CatalogFilters + description: CatalogFilters operations. + - name: CatalogItemUserActionMessageDefinitions + description: User actions standardized messages definitions + - name: Files + description: File operations. + - name: SchemaValidations + description: Schema Validations operations. + - name: ProvisionerActions + description: Provisioning notifications from AWX/Provisioner +paths: + /project/{projectKey}/components: + get: + tags: + - Project-components + summary: Returns the information of the project's components in the Bitbucket repository. + operationId: getProjectComponents + parameters: + - name: projectKey + in: path + description: project key. + required: true + schema: + type: string + - name: accessToken + in: query + description: access token for azure queries. + required: true + schema: + type: string + responses: + "200": + description: A list of Project Component Information + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ProjectComponentInfo' + "401": + description: Invalid client token on the request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "403": + description: Insufficient permissions for the client to access the resource. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + /catalog-descriptors: + get: + tags: + - Catalog-descriptors + summary: List of all available Catalog Descriptors. + description: > + Returns a list of all available Catalog Descriptors.
+ operationId: getCatalogDescriptors + responses: + "200": + description: A list of Catalog Descriptors. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CatalogDescriptor' + "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' + /catalogs/{catalogId}: + get: + tags: + - Catalogs + summary: Get a catalog by id. + description: > + Returns a valid catalog. + operationId: getCatalog + parameters: + - name: catalogId + in: path + description: id for the Catalog. + required: true + schema: + type: string + responses: + "200": + description: A Single catalog. + content: + application/json: + schema: + $ref: '#/components/schemas/Catalog' + "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' + "404": + description: Catalog not found + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + /catalog-items: + get: + tags: + - CatalogItems + summary: List of all CatalogItems. + description: > + Returns a list of all CatalogItems for the given Catalog identified by catalogId.
+ CatalogItems referenced on a Catalog that are either invalid or non-existent are **excluded** from the response. + operationId: getCatalogItems + parameters: + - name: catalogId + in: query + description: id for the Catalog. + required: true + schema: + type: string + - name: sortByTitle + in: query + description: Sort the returned CatalogItems by title, either in ascending or descending order. + required: true + schema: + $ref: '#/components/schemas/SortOrder' + example: 'asc' + responses: + "200": + description: A list of valid CatalogItems. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CatalogItem' + "400": + description: Invalid parameters provided on the request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "401": + description: Invalid client token on the request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "403": + description: Insufficient permissions for the client to access the resource. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + /catalog-items/{id}: + get: + tags: + - CatalogItems + summary: Returns the CatalogItem associated to the provided id. + description: > + Returns the CatalogItem associated to the provided id, unless: +
    +
  • The id is not associated to any CatalogItem.
  • +
  • Or the associated CatalogItem is invalid and can't be processed to create a response.
  • +
+ operationId: getCatalogItemById + parameters: + - name: id + in: path + description: id for the CatalogItem. + required: true + schema: + type: string + example: 'aSdFam...yCg==' + responses: + "200": + description: The CatalogItem. + content: + application/json: + schema: + $ref: '#/components/schemas/CatalogItem' + "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' + "404": + description: No CatalogItem associated to the provided id. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "422": + description: Invalid CatalogItem associated to the provided id. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + /{projectKey}/catalog-items: + get: + tags: + - CatalogItems + summary: List of all CatalogItems given a project key. + description: > + Returns a list of all CatalogItems for the given Catalog identified by catalogId given a project key.
+ CatalogItems referenced on a Catalog that are either invalid or non-existent are **excluded** from the response. + operationId: getCatalogItemsForProjectKey + parameters: + - name: catalogId + in: query + description: id for the Catalog. + required: true + schema: + type: string + - name: accessToken + in: query + description: access token for azure queries. + required: true + schema: + type: string + - name: sortByTitle + in: query + description: Sort the returned CatalogItems by title, either in ascending or descending order. + required: true + schema: + $ref: '#/components/schemas/SortOrder' + example: 'asc' + - name: projectKey + in: path + description: project key. + required: true + schema: + type: string + responses: + "200": + description: A list of valid CatalogItems. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CatalogItem' + "400": + description: Invalid parameters provided on the request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "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' + /{projectKey}/catalog-items/{id}: + get: + tags: + - CatalogItems + summary: Returns the CatalogItem associated to the provided id, given a project key. + description: > + Returns the CatalogItem associated to the provided id, given a project key, unless: +
    +
  • The id is not associated to any CatalogItem.
  • +
  • Or the associated CatalogItem is invalid and can't be processed to create a response.
  • +
  • Project key does not exist or user has no visibility over it
  • +
+ operationId: getCatalogItemByIdForProjectKey + parameters: + - name: id + in: path + description: id for the CatalogItem. + required: true + schema: + type: string + example: 'aSdFam...yCg==' + - name: projectKey + in: path + description: project key. + required: true + schema: + type: string + - name: accessToken + in: query + description: access token for azure queries. + required: true + schema: + type: string + responses: + "200": + description: The CatalogItem. + content: + application/json: + schema: + $ref: '#/components/schemas/CatalogItem' + "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' + "404": + description: No CatalogItem associated to the provided id. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "422": + description: Invalid CatalogItem associated to the provided id. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + /catalog-filters: + get: + tags: + - CatalogFilters + summary: List of all CatalogItemFilters. + description: > + Returns the list of all CatalogItemFilters for the CatalogItems on the Catalog identified by catalogId.
+ CatalogItemFilters are built based on the contents of the Catalog and its CatalogItems.
+ Catalog or CatalogItems **with errors** will affect the number and/or contents of the returned CatalogItemFilters. + operationId: getCatalogFilters + parameters: + - name: catalogId + in: query + description: id for the Catalog. + required: true + schema: + type: string + example: 'cHJvam...yCg==' + responses: + "200": + description: A list of CatalogItemFilters. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CatalogItemFilter' + "400": + description: Invalid parameters provided on the request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "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' + /files/{id}/contents: + get: + tags: + - Files + summary: Returns the contents of a File. + description: > + Returns the contents of a File entity associated to the provided id, unless: +
    +
  • The id is not associated to any File.
  • +
  • Or the associated File is invalid (e.g. corrupted) and can't be processed to create a response.
  • +
+ operationId: getFileById + parameters: + - name: id + in: path + description: id for the File. + required: true + schema: + type: string + example: 'cHJvam...yCg==' + - name: format + in: query + description: desired format for the returned File contents, **must** match the actual format. + required: true + schema: + $ref: '#/components/schemas/FileFormat' + example: image + responses: + "200": + description: File contents, either in binary or text format. + content: + "application/octet-stream": + schema: + type: string + format: byte + description: binary file contents. + example: '' + "text/*": + schema: + type: string + description: text file contents. + example: '# About\nThis repository contains the source code for...' + "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' + "404": + description: No File associated to the provided id. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "422": + description: Invalid File associated to the provided id. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + /catalog-items/{catalogItemId}/user-actions/{userActionId}/messages-definitions/{messageDefinitionId}: + post: + tags: + - CatalogItemUserActionMessageDefinitions + summary: Get a message definition by id. + description: > + Returns an standard message definition + operationId: getMessageDefinitionByCatalogItemIdAndMessageId + parameters: + - name: catalogItemId + in: path + description: id for the CatalogItem + required: true + schema: + type: string + - name: userActionId + in: path + description: id for the CatalogItemUserAction + required: true + schema: + type: string + - name: messageDefinitionId + in: path + description: id for the CatalogItemUserActionMessageDefinition + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: + type: string + responses: + "200": + description: A single message definition. + content: + application/json: + schema: + $ref: '#/components/schemas/CatalogItemUserActionMessageDefinition' + "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' + "404": + description: Catalog not found + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + /schema-validation/{className}: + post: + tags: + - SchemaValidations + summary: Validate a yaml against a proper schema. + description: > + Validates the provided Catalog schema against the expected format and structure.
+ Returns a 200 OK response if the schema is valid, otherwise returns a 400 Bad Request with details about the validation errors. + operationId: validateCatalogSchema + parameters: + - name: className + in: path + description: ClassName for the uploaded file, so we can get proper schema for validation. + required: true + schema: + type: string + requestBody: + required: true + content: + multipart/form-data: + schema: + type: object + properties: + file: + type: string + format: binary + responses: + "200": + description: Validation resul. + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationResult' + '400': + description: Invalid input or validation failed + + /provision/{project-key}/{status}: + put: + tags: + - ProvisionerActions + summary: Create new project component + description: > + This endpoint will create a new project component. + operationId: notifyProvisioningStatusUpdate + parameters: + - name: project-key + in: path + description: Project key of the provisioned component. + required: true + schema: + type: string + - name: status + in: path + description: Provisioning status for the component. + required: true + example: CREATING + schema: + type: string + enum: [ CREATING, CREATED, FAILED, DELETING, UNKNOWN ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProvisioningStatusUpdateRequest' + responses: + "200": + description: Provisioning status update completed. + "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: + - ProvisionerActions + summary: Update an existing project component + description: > + This endpoint receives provisioning status update notifications from AWX. + operationId: notifyProvisioningStatusUpdatePartially + parameters: + - name: project-key + in: path + description: Project key of the provisioned component. + required: true + schema: + type: string + - name: status + in: path + description: Provisioning status for the component. + required: true + example: CREATING + schema: + type: string + enum: [ CREATING, CREATED, FAILED, DELETING, UNKNOWN ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProvisioningStatusUpdateRequest' + responses: + "200": + description: Provisioning status update completed. + "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. + + /provision/{project-key}: + delete: + security: + - basicAuth: [] # Enable ONLY basicAuth + tags: + - ProvisionerActions + summary: Delete provision status component from the file + description: > + This endpoint receives provisioning status delete notifications from Component Provisioner. + operationId: deleteProvisioningStatus + parameters: + - name: project-key + in: path + description: Project key of the provisioned component. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProvisioningDeleteRequest' + responses: + "200": + description: Project component properly deleted. + "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. + +components: + securitySchemes: + bearerAuth: + type: http + description: > + Authorization via bearer token. + scheme: bearer + bearerFormat: JWT + basicAuth: + type: http + scheme: basic + description: Basic authentication for internal provisioning endpoint + schemas: + Catalog: + properties: + name: + type: string + example: 'catalog-name' + description: + type: string + example: 'A brief description for a catalog' + communityPageId: + type: string + example: 'aSdFam...yCg==' + links: + type: array + items: + $ref: '#/components/schemas/CatalogLink' + tags: + type: array + items: + type: string + example: + - 'tasks' + - 'technologies' + ProjectComponentInfo: + properties: + componentId: + type: string + example: 'edpc-4132-v2' + status: + type: string + example: 'CREATING' + canBeDeleted: + type: boolean + example: true + logoUrl: + type: string + example: https://somepic.jpg + componentUrl: + type: string + example: 'https://bitbucket.com/projects/CATALOGS/repos/project-components/browse/projects' + CatalogDescriptor: + properties: + id: + type: string + example: 'aSdFam...yCg==' + slug: + type: string + example: 'aSdFam...yCg==' + CatalogLink: + properties: + url: + type: string + example: 'http://some-link.com' + name: + type: string + example: 'whatever name' + CatalogItem: + properties: + id: + type: string + example: 'aSdFam...yCg==' + path: + type: string + example: projects/SOMEPROJECT/repos/some-repo/raw/CatalogItem.yaml?at=refs/heads/master + title: + type: string + example: An item title + shortDescription: + type: string + example: This is a short description for the item + descriptionFileId: + type: string + example: cHJvam...0ZXIK + imageFileId: + type: string + example: cHJvam...YXN0Z + itemSrc: + type: string + example: 'https://bitbucket.some-company.com/projects/SOMEPROJECT/repos/some-repo/browse/CatalogItem.yaml?at=refs/heads/master' + tags: + type: array + items: + $ref: '#/components/schemas/CatalogItemTag' + authors: + type: array + items: + type: string + example: + - '@SomeAuthor' + - '@SomeOtherAuthor' + date: + type: string + format: date-time + example: '2021-07-01T00:00:00Z' + userActions: + type: array + items: + $ref: '#/components/schemas/CatalogItemUserAction' + restrictions: + $ref: '#/components/schemas/CatalogItemRestriction' + required: + - id + - title + - shortDescription + - description + - image + - authors + - date + example: + id: aSdFam...yCg== + path: projects/SOMEPROJECT/repos/some-repo/raw/CatalogItem.yaml?at=refs/heads/master + title: An item title + shortDescription: This is a short description for the item + descriptionFileId: cHJvam...0ZXIK + imageFileId: cHJvam...YXN0Z + itemSrc: https://bitbucket.some-company.com/projects/SOMEPROJECT/repos/some-repo/browse/CatalogItem.yaml?at=refs/heads/master + tags: + - label: data + options: + - 'some-data-option' + - 'some-other-data-option' + authors: + - '@SomeAuthor' + - '@SomeOtherAuthor' + date: "2021-07-01T00:00:00Z" + CatalogItemUserAction: + properties: + id: + type: string + nullable: false + example: 'CODE' + displayName: + type: string + nullable: false + example: 'View Code' + url: + type: string + nullable: true + example: 'https://quickstarter' + triggerMessage: + type: string + nullable: true + example: 'Provisioning a component' + requestable: + type: boolean + nullable: true + example: true + restrictionMessage: + type: string + nullable: true + example: 'You do not have permissions to provision this component.' + parameters: + type: array + items: + $ref: '#/components/schemas/CatalogItemUserActionParameter' + example: + id: "PROVISION" + triggerMessage: "Provisioning a component custom message" + parameters: + - name: "workflow" + type: "string" + required: true + defaultValue: "9987" + description: "Workflow to execute." + visible: false + CatalogItemUserActionMessageDefinition: + properties: + id: + type: string + nullable: false + example: 'OPENSHIFT_CONNECTION_ERROR' + type: + $ref: '#/components/schemas/CatalogItemUserActionMessageType' + title: + type: string + nullable: false + example: 'An error occurred while connecting to OpenShift' + message: + type: string + nullable: false + example: > + Authorization error: please check your user credentials for deployment + and try again later. + createsIncident: + type: boolean + nullable: false + example: + id: "OPENSHIFT_CONNECTION_ERROR" + title: "An error occurred while connecting to OpenShift" + message: > + Authorization error: please check your user credentials for deployment + and try again later. + CatalogItemUserActionMessageType: + type: string + enum: + - success + - error + example: error + CatalogItemUserActionParameter: + properties: + name: + type: string + nullable: false + example: 'workflow' + type: + type: string + nullable: false + example: 'string' + required: + type: boolean + example: 'true' + defaultValue: + type: string + nullable: true + example: '123' + defaultValues: + nullable: true + type: array + items: + type: string + example: + - 'some-data-option' + - 'some-other-data-option' + options: + nullable: true + type: array + items: + type: string + example: + - 'some-data-option' + - 'some-other-data-option' + locations: + type: array + nullable: true + items: + $ref: '#/components/schemas/CatalogItemUserActionParameterLocation' + label: + type: string + nullable: false + example: 'Workflow to execute.' + placeholder: + type: string + nullable: true + example: 'some placeholder for a workflow' + hint: + type: string + nullable: true + example: 'some hint for a workflow' + sendOnDeletion: + type: boolean + nullable: true + example: false + visible: + type: boolean + example: 'true' + validations: + type: array + nullable: true + items: + $ref: '#/components/schemas/CatalogItemUserActionParameterValidation' + example: + name: "workflow" + type: "string" + required: true + defaultValue: "9987" + description: "Workflow to execute." + visible: false + CatalogItemUserActionParameterValidation: + properties: + regex: + type: string + example: '/^[a-z\s]{0,255}$/i' + errorMessage: + type: string + example: 'There is an error in the provided value, please check it and try again.' + CatalogItemUserActionParameterLocation: + properties: + location: + type: string + example: 'EU' + value: + type: string + example: '1234' + CatalogItemTag: + properties: + label: + type: string + example: data + options: + type: array + uniqueItems: true + items: + type: string + example: + - 'some-data-option' + - 'some-other-data-option' + CatalogItemFilter: + properties: + label: + type: string + example: business + options: + type: array + uniqueItems: true + items: + type: string + example: + - 'some-business-option' + - 'some-other-business-option' + example: + label: business + options: + - 'some-business-option' + - 'some-other-business-option' + CatalogItemRestriction: + properties: + projects: + type: array + uniqueItems: true + items: + type: string + example: + - 'project-key-1' + - 'project-key-2' + SortOrder: + type: string + enum: + - asc + - desc + example: asc + FileFormat: + type: string + enum: + - image + - markdown + - yaml + example: markdown + RestErrorMessage: + properties: + message: + type: string + required: + - message + ValidationResult: + type: object + properties: + valid: + type: boolean + errors: + type: array + items: + $ref: '#/components/schemas/ValidationMessage' + ValidationMessage: + type: object + properties: + type: + type: string + code: + type: string + message: + type: string + required: + - message + + # === New, explicit request models to avoid sharing === + ProvisioningStatusUpdateRequest: + 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" + + componentUrl: + type: string + description: the repository url where the component was provisioned + example: "https://bitbucket.com/projects/DEVSTACK/repos/devstack-component-catalog" + nullable: true + + accessToken: + type: string + description: the access token to be used to get azure groups + example: "some-access-token" + nullable: false + + parameters: + type: array + description: List of name/value string parameters. + items: + type: object + required: + - name + - value + 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: + 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" + + parameters: + type: array + description: List of name/value string parameters. + items: + type: object + required: + - name + - value + properties: + name: + type: string + description: Parameter name + example: "environment" + values: + type: array + description: Parameter values + items: + type: string + example: + - "production" + - "staging" \ 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 new file mode 100644 index 0000000..fb49f6d --- /dev/null +++ b/external-service-marketplace/openapi/openapi-component_provisioner-v1.0.0.yaml @@ -0,0 +1,411 @@ +openapi: 3.0.3 +info: + title: Component Provisioner REST API + version: '1.0.0' + description: > + The Component Provisioner API allows clients to trigger Ansible Automation Platform (AWX) workflows. + + **NOTES**: + - The OpenAPI specification file is also used to [generate](https://openapi-generator.tech/) REST client(s) and a server REST API. + - Clients and servers generated from the same OpenAPI specification version are guaranteed to be **compatible**. + contact: + name: EDPCore Team + url: https://confluence.biscrum.com/pages/viewpage.action?spaceKey=EDP&title=Welcome +servers: + - url: http://{baseurl}/v1 + variables: + baseurl: + default: localhost:8080 + description: Default address for a Component Provisioner's backend REST API instance. +security: + - bearerAuth: [] +tags: + - name: ProvisionerActions + description: ProvisionerAction operations. + - name: ProvisionerMessagesDefinitions + description: Provisioner standardized messages definitions + - name: ProvisionResults + description: Work with project components statuses +paths: + /provision-actions: + post: + tags: + - ProvisionerActions + summary: Execute a provisioning action with parameters + description: > + This endpoint receives ProvisionerActions from clients and triggers them in AWX. + operationId: triggerProvisionAction + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProvisionAction' + responses: + "201": + description: Provisioning created. + content: + application/json: + schema: + $ref: '#/components/schemas/ProvisionActionResponse' + "401": + description: Invalid client token on the request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "403": + description: Insufficient permissions for the client to access the resource. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + + /catalog-items/{catalogItemId}/user-actions/{action}/message-definitions/{id}: + post: + tags: + - ProvisionerMessagesDefinitions + summary: Get a message definition by catalogItemId and id. + description: > + Returns an standard message definition + operationId: getMessageDefinitionByCatalogItemIdAndMessageId + parameters: + - name: catalogItemId + in: path + description: id for the Catalog Item where Message is defined. + required: true + schema: + type: string + - name: action + in: path + description: Action for the MessageDefinition. + required: true + schema: + type: string + - name: id + in: path + description: id for the MessageDefinition. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: object + additionalProperties: + type: string + responses: + "200": + description: A single message definition. + content: + application/json: + schema: + $ref: '#/components/schemas/ProvisionerMessageDefinition' + "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' + "404": + description: Catalog not found + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + + /provision/{projectKey}/{status}: + put: + tags: + - ProvisionResults + summary: Notify provisioning Status Update + description: > + This endpoint receives provisioning status update notifications from AWX. + operationId: notifyProvisioningStatusUpdate + 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: + 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" + componentUrl: + type: string + description: The bitbucket repository url for the provisioned component. + example: "https://bitbucket.com/projects/myproject/repos/repo_name" + 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. + + /provision/{projectKey}: + delete: + security: + - basicAuth: [ ] # Enable ONLY basicAuth + tags: + - ProvisionResults + summary: Delete provision status component from the file + description: > + This endpoint receives provisioning status delete notifications from Component Provisioner. + operationId: deleteProvisioningStatus + parameters: + - name: projectKey + in: path + description: Project key of the provisioned component. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ProvisioningDeleteRequest' + responses: + "200": + description: Project component properly deleted. + "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. + + /support/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 + parameters: + - name: projectKey + in: path + description: Project key of the provisioned component. + required: true + schema: + type: string + - name: componentId + in: path + description: Component id of the provisioned component. + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/CreateIncidentAction' + responses: + "201": + description: Incident properly created. + content: + application/json: + schema: + $ref: '#/components/schemas/ProvisionActionResponse' + "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: + bearerAuth: + type: http + description: > + Authorization via bearer token. + scheme: bearer + bearerFormat: JWT + basicAuth: + type: http + scheme: basic + description: Basic authentication for internal provisioning endpoint + schemas: + ProvisionAction: + properties: + id: + type: string + nullable: false + example: 'PROVISION' + parameters: + type: array + items: + $ref: '#/components/schemas/ProvisionActionParameter' + example: + id: "PROVISION" + triggerMessage: "Provisioning a component custom message" + parameters: + - name: "workflow" + type: "string" + required: true + defaultValue: "2558" + description: "Workflow to execute." + visible: false + CreateIncidentAction: + properties: + parameters: + type: array + items: + $ref: '#/components/schemas/CreateIncidentParameter' + example: + parameters: + - name: "cluster_location" + type: "string" + value: "eu" + ProvisionActionParameter: + properties: + name: + type: string + nullable: false + example: 'workflow' + type: + type: string + nullable: false + example: 'string' + value: + type: object + nullable: false + example: '2558' + example: + name: "workflow" + type: "string" + value: "2558" + ProvisionActionResponse: + properties: + failed: + title: Job failed to execute + type: boolean + id: + title: Job ID + type: integer + created: + title: Job created timestamp + type: string + format: date-time + modified: + title: Job modified timestamp + type: string + format: date-time + ProvisionerMessageDefinition: + properties: + id: + type: string + nullable: false + example: 'OPENSHIFT_CONNECTION_ERROR' + type: + $ref: '#/components/schemas/ProvisionerMessageDefinitionType' + title: + type: string + nullable: false + example: 'An error occurred while connecting to OpenShift' + message: + type: string + nullable: false + example: > + Authorization error: please check your user credentials for deployment + and try again later. + createsIncident: + type: boolean + nullable: false + example: + id: "OPENSHIFT_CONNECTION_ERROR" + title: "An error occurred while connecting to OpenShift" + message: > + Authorization error: please check your user credentials for deployment + and try again later. + ProvisionerMessageDefinitionType: + type: string + enum: + - success + - error + example: error + RestErrorMessage: + properties: + message: + type: string + required: + - message + + ProvisioningDeleteRequest: + type: object + properties: + componentId: + type: string + description: The componentId set by the user. + example: "any-component-id-from-backend" + + CreateIncidentParameter: + properties: + name: + type: string + nullable: false + example: 'workflow' + type: + type: string + nullable: false + example: 'string' + value: + type: object + nullable: false + example: '2558' + example: + name: "workflow" + type: "string" + value: "2558" \ No newline at end of file diff --git a/external-service-marketplace/pom.xml b/external-service-marketplace/pom.xml index 6358551..d5134ea 100644 --- a/external-service-marketplace/pom.xml +++ b/external-service-marketplace/pom.xml @@ -87,15 +87,137 @@ jakarta.annotation-api + + jakarta.validation + jakarta.validation-api + + + + io.swagger.core.v3 + swagger-annotations + 2.2.21 + + + + com.google.code.findbugs + jsr305 + 3.0.2 + provided + + + + org.apache.httpcomponents + httpclient + 4.5.14 + provided + + + org.springframework.boot spring-boot-starter-cache + + javax.annotation + javax.annotation-api + 1.3.2 + compile + + + org.jetbrains + annotations + 17.0.0 + compile + + + com.google.code.findbugs + jsr305 + 3.0.2 + compile + + + org.openapitools + openapi-generator-maven-plugin + + + generate-marketplace-catalog-client + + generate + + + FILTER=operationId:getProjectComponents + java + ${project.basedir} + 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 + false + true + true + true + false + false + false + false + + false + true + true + true + true + + + java8 + jackson + true + + + + + generate-marketplace-provisioner-client + + generate + + + FILTER=operationId:triggerProvisionAction|notifyProvisioningStatusUpdate|createIncident + java + ${project.basedir} + 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 + false + true + true + true + false + false + false + false + + false + true + true + true + true + + + java8 + jackson + 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 new file mode 100644 index 0000000..995ec5a --- /dev/null +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClient.java @@ -0,0 +1,73 @@ +package org.opendevstack.apiservice.externalservice.marketplace.client; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.openapitools.jackson.nullable.JsonNullableModule; +import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig.MarketplaceInstanceConfig; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.ApiClient; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.auth.HttpBearerAuth; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.web.client.RestTemplate; + +@Getter +@Slf4j +public class MarketplaceApiClient { + + + private final String instanceName; + private final MarketplaceInstanceConfig config; + private final ApiClient apiClient; + + /** + * Constructor for MarketplaceApiClient. + * + * @param instanceName Name of the Marketplace instance + * @param config Configuration for this instance + * @param restTemplate RestTemplate configured with appropriate timeouts and SSL settings + */ + public MarketplaceApiClient(String instanceName, MarketplaceInstanceConfig config, RestTemplate restTemplate) { + this.instanceName = instanceName; + this.config = config; + + // Configure ObjectMapper with JsonNullableModule for the RestTemplate + configureRestTemplateWithJsonNullable(restTemplate); + + // Initialize the generated ApiClient + this.apiClient = new ApiClient(restTemplate); + + // Configure authentication – prefer bearer token over basic auth + if (config.getBearerToken() != null && !config.getBearerToken().isEmpty()) { + HttpBearerAuth auth = (HttpBearerAuth) this.apiClient.getAuthentication("bearerAuth"); + auth.setBearerToken(config.getBearerToken()); + log.info("MarketplaceApiClient initialized for instance '{}' with bearer token authentication", + instanceName); + } else if (config.getUsername() != null && config.getPassword() != null) { + this.apiClient.setUsername(config.getUsername()); + this.apiClient.setPassword(config.getPassword()); + log.info("MarketplaceApiClient initialized for instance '{}' with basic authentication", + instanceName); + } else { + log.warn("MarketplaceApiClient initialized for instance '{}' without authentication " + + "(neither bearer token nor username/password provided)", instanceName); + } + } + + /** + * Configure RestTemplate's ObjectMapper to handle JsonNullable types. + * + * @param restTemplate RestTemplate to configure + */ + private void configureRestTemplateWithJsonNullable(RestTemplate restTemplate) { + for (HttpMessageConverter converter : restTemplate.getMessageConverters()) { + if (converter instanceof MappingJackson2HttpMessageConverter jacksonConverter) { + ObjectMapper objectMapper = jacksonConverter.getObjectMapper(); + objectMapper.registerModule(new JsonNullableModule()); + log.debug("Registered JsonNullableModule with ObjectMapper for instance '{}'", instanceName); + return; + } + } + log.warn("No MappingJackson2HttpMessageConverter found in RestTemplate 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 new file mode 100644 index 0000000..d5c15bc --- /dev/null +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClientFactory.java @@ -0,0 +1,204 @@ +package org.opendevstack.apiservice.externalservice.marketplace.client; + +import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig; +import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig.MarketplaceInstanceConfig; +import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceClientException; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.X509Certificate; +import java.util.Map; +import java.util.Set; + +@Component +@Slf4j +public class MarketplaceApiClientFactory { + + private final MarketplaceServiceConfig configuration; + private final RestTemplateBuilder restTemplateBuilder; + + /** + * Constructor with dependency injection. + * + * @param configuration Marketplace service configuration + * @param restTemplateBuilder RestTemplate builder for creating HTTP clients + */ + public MarketplaceApiClientFactory(MarketplaceServiceConfig configuration, + RestTemplateBuilder restTemplateBuilder) { + this.configuration = configuration; + this.restTemplateBuilder = restTemplateBuilder; + + log.info("MarketplaceApiClientFactory initialized with {} instance(s)", + configuration.getInstances().size()); + } + + /** + * Resolve the effective instance name. + *
    + *
  • If the default instance is configured via {@code externalservices.marketplace.default-instance}, it is returned.
  • + *
  • Otherwise the first entry of the instances map is returned (insertion order).
  • + *
  • If no instances are configured at all, a {@link MarketplaceClientException} is thrown.
  • + *
+ * + * @return The resolved instance name (never {@code null}/blank) + * @throws MarketplaceClientException if no Marketplace instances are configured + */ + public String getDefaultInstanceName() throws MarketplaceClientException { + + String defaultInstance = configuration.getDefaultInstance(); + if (defaultInstance != null && !defaultInstance.isBlank()) { + return defaultInstance; + } + + Map instances = configuration.getInstances(); + if (instances == null || instances.isEmpty()) { + throw new MarketplaceClientException("No Marketplace instances configured"); + } + + return instances.keySet().iterator().next(); + } + + /** + * Get a {@link MarketplaceApiClient} for a specific instance. + * If {@code instanceName} is {@code null} or blank, the default instance is used. + * + * @param instanceName Name of the Marketplace instance, or {@code null}/{@code ""} for the default + * @return Configured MarketplaceApiClient + * @throws MarketplaceClientException if the instance is not configured + */ + @Cacheable(value = "marketplaceApiClients", key = "#instanceName", condition = "#instanceName != null && !#instanceName.isBlank()") + public MarketplaceApiClient getClient(String instanceName) throws MarketplaceClientException { + if (instanceName == null || instanceName.isBlank()) { + throw new MarketplaceClientException( + String.format("Provide instance name. Available instances: %s", + configuration.getInstances().keySet())); + } + + MarketplaceInstanceConfig instanceConfig = configuration.getInstances().get(instanceName); + + if (instanceConfig == null) { + throw new MarketplaceClientException( + String.format("Marketplace instance '%s' is not configured. Available instances: %s", + instanceName, configuration.getInstances().keySet())); + } + + log.info("Creating new MarketplaceApiClient for instance '{}'", instanceName); + + RestTemplate restTemplate = createRestTemplate(instanceConfig); + return new MarketplaceApiClient(instanceName, instanceConfig, restTemplate); + } + + /** + * Get the default client, as determined by {@code externalservices.marketplace.default-instance}. + * Falls back to the first configured instance when {@code default-instance} is not set. + * + * @return MarketplaceApiClient for the default instance + * @throws MarketplaceClientException if no instances are configured + */ + @Cacheable(value = "marketplaceApiClients", key = "'default'") + public MarketplaceApiClient getClient() throws MarketplaceClientException { + String defaultInstanceName = getDefaultInstanceName(); + MarketplaceInstanceConfig instanceConfig = configuration.getInstances().get(defaultInstanceName); + RestTemplate restTemplate = createRestTemplate(instanceConfig); + + return new MarketplaceApiClient(defaultInstanceName, instanceConfig, restTemplate); + } + + /** + * Get all available instance names. + * + * @return Set of configured instance names + */ + public Set getAvailableInstances() { + return configuration.getInstances().keySet(); + } + + /** + * Check if an instance is configured. + * + * @param instanceName Name of the instance to check + * @return true if configured, false otherwise + */ + public boolean hasInstance(String instanceName) { + return configuration.getInstances().containsKey(instanceName); + } + + /** + * Clear the client cache (useful for testing or when configuration changes). + */ + @CacheEvict(value = "marketplaceApiClients", allEntries = true) + public void clearCache() { + log.info("Clearing MarketplaceApiClient cache"); + } + + /** + * Create a configured RestTemplate for a Marketplace instance. + * + * @param config Configuration for the instance + * @return Configured RestTemplate + */ + private RestTemplate createRestTemplate(MarketplaceInstanceConfig config) { + RestTemplate restTemplate = restTemplateBuilder.build(); + + SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory(); + requestFactory.setConnectTimeout(config.getConnectionTimeout()); + requestFactory.setReadTimeout(config.getReadTimeout()); + restTemplate.setRequestFactory(requestFactory); + + if (config.isTrustAllCertificates()) { + log.warn("Trust all certificates is enabled for Marketplace connection. " + + "This should only be used in development environments!"); + configureTrustAllCertificates(restTemplate); + } + + return restTemplate; + } + + /** + * Configure RestTemplate to trust all SSL certificates. + * WARNING: This should only be used in development environments. + * + * @param restTemplate RestTemplate to configure + */ + @SuppressWarnings({"java:S4830", "java:S1186"}) + private void configureTrustAllCertificates(RestTemplate restTemplate) { + try { + TrustManager[] trustAllCerts = new TrustManager[]{ + new X509TrustManager() { + public X509Certificate[] getAcceptedIssuers() { + return new X509Certificate[0]; + } + // Intentionally empty - trusting all certificates for development environments + public void checkClientTrusted(X509Certificate[] certs, String authType) { + // No validation performed - development only + } + // Intentionally empty - trusting all certificates for development environments + public void checkServerTrusted(X509Certificate[] certs, String authType) { + // No validation performed - development only + } + } + }; + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + + HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); + // Intentionally disabling hostname verification for development environments + HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true); + + } catch (NoSuchAlgorithmException | KeyManagementException e) { + log.error("Failed to configure SSL trust all certificates", e); + } + } +} diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceServiceConfig.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceServiceConfig.java new file mode 100644 index 0000000..ad7adce --- /dev/null +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceServiceConfig.java @@ -0,0 +1,85 @@ +package org.opendevstack.apiservice.externalservice.marketplace.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +@ConfigurationProperties(prefix = "externalservices.marketplace") +@Data +public class MarketplaceServiceConfig { + + /** + * Name of the default Marketplace instance to use when no instance name is provided. + * If not set, the first configured instance is used as default. + */ + private String defaultInstance; + + /** + * Map of Marketplace instances with the instance name as the key and the configuration as the value. + */ + private Map instances = new HashMap<>(); + + /** + * Configuration for a single Jira instance. + */ + @Data + public static class MarketplaceInstanceConfig { + + /** + * The project components base URL of the Marketplace + */ + private String projectComponentsBaseUrl; + + /** + * The provisioner actions base URL of the Marketplace + */ + private String provisionerActionsBaseUrl; + + /** + * The provision results base URL of the Marketplace + */ + private String provisionResultsBaseUrl; + + /** + * Authentication access token for accessing the Marketplace API + */ + private String accessToken; + + /** + * Authentication bearer token for accessing the Marketplace API + */ + private String bearerToken; + + /** + * Username for authentication (used with password for basic auth). + * Only used if bearerToken is not provided. + */ + private String username; + + /** + * Password or personal access token for authentication. + * Only used if bearerToken is not provided. + */ + private String password; + + /** + * Connection timeout in milliseconds (default: 30000) + */ + private int connectionTimeout = 30000; + + /** + * Read timeout in milliseconds (default: 30000). + */ + private int readTimeout = 30000; + + /** + * Whether to trust all SSL certificates (default: false). + * WARNING: Should only be used in development environments. + */ + private boolean trustAllCertificates = false; + } +} diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/exception/MarketplaceClientException.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/exception/MarketplaceClientException.java new file mode 100644 index 0000000..4a74d25 --- /dev/null +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/exception/MarketplaceClientException.java @@ -0,0 +1,12 @@ +package org.opendevstack.apiservice.externalservice.marketplace.exception; + +public class MarketplaceClientException extends Exception { + + public MarketplaceClientException(String message) { + super(message); + } + + public MarketplaceClientException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/model/CreateComponentParameter.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/model/CreateComponentParameter.java deleted file mode 100644 index a4409c3..0000000 --- a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/model/CreateComponentParameter.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.opendevstack.apiservice.externalservice.marketplace.model; - - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@NoArgsConstructor -@AllArgsConstructor -@Data -public class CreateComponentParameter { - - private String name; - private String type; - private String value; - -} diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/model/ProjectComponent.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/model/ProjectComponent.java deleted file mode 100644 index 89b640b..0000000 --- a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/model/ProjectComponent.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.opendevstack.apiservice.externalservice.marketplace.model; - -import lombok.AllArgsConstructor; -import lombok.Data; -import lombok.NoArgsConstructor; - -@NoArgsConstructor -@AllArgsConstructor -@Data -public class ProjectComponent { - - private String componentId; - private String status; - private boolean canBeDeleted; - private String logoUrl; - private String componentUrl; - -} 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 8873f42..eeb0d75 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 @@ -1,15 +1,29 @@ package org.opendevstack.apiservice.externalservice.marketplace.service; import org.opendevstack.apiservice.externalservice.api.ExternalService; -import org.opendevstack.apiservice.externalservice.marketplace.model.CreateComponentParameter; -import org.opendevstack.apiservice.externalservice.marketplace.model.ProjectComponent; +import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceClientException; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; import java.util.List; +import java.util.Set; public interface MarketplaceService extends ExternalService { + Set getAvailableInstances(); - ProjectComponent getProjectComponent(String projectId, String componentId); + boolean hasInstance(String instanceName); + + String getDefaultInstance() throws MarketplaceClientException; + + ProjectComponentInfo getProjectComponent(String projectId, String componentId) throws MarketplaceClientException; + + ProjectComponentInfo getProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceClientException; + + boolean provisionProjectComponent(String projectId, List params) throws MarketplaceClientException; + + boolean deleteProjectComponent(String projectId, String componentId) throws MarketplaceClientException; + + void registerProjectComponent(String projectId, String componentId) throws MarketplaceClientException; - ProjectComponent createProjectComponent(String projectId, List params); } 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 new file mode 100644 index 0000000..6176415 --- /dev/null +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/impl/MarketplaceServiceImpl.java @@ -0,0 +1,131 @@ +package org.opendevstack.apiservice.externalservice.marketplace.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.externalservice.marketplace.client.MarketplaceApiClient; +import org.opendevstack.apiservice.externalservice.marketplace.client.MarketplaceApiClientFactory; +import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceClientException; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.ApiClient; +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.model.CreateIncidentAction; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.NotifyProvisioningStatusUpdateRequest; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; +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.service.MarketplaceService; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestClientException; + +import java.util.List; +import java.util.Set; + +@Service +@Slf4j +public class MarketplaceServiceImpl implements MarketplaceService { + + private final MarketplaceApiClientFactory clientFactory; + + public MarketplaceServiceImpl(MarketplaceApiClientFactory clientFactory) { + this.clientFactory = clientFactory; + log.info("MarketplaceServiceImpl initialized"); + } + + @Override + public ProjectComponentInfo getProjectComponent(String projectId, String componentId) throws MarketplaceClientException { + return getProjectComponent(getDefaultInstance(), projectId, componentId); + } + + @Override + public ProjectComponentInfo getProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceClientException { + log.debug("Marketplace service GET component with id {} for project {} in instance {} ", componentId, projectId, instanceName); + try { + MarketplaceApiClient marketplaceClient = clientFactory.getClient(instanceName); + ApiClient apiClient = marketplaceClient.getApiClient(); + + ProjectComponentsApi projectComponentsApi = new ProjectComponentsApi(apiClient); + apiClient.setBasePath(marketplaceClient.getConfig().getProjectComponentsBaseUrl()); + + return projectComponentsApi.getProjectComponents(projectId, marketplaceClient.getConfig().getAccessToken()).stream() + .filter(component -> component.getComponentId().equals(componentId)) + .findFirst() + .orElse(null); + } catch (HttpClientErrorException.Unauthorized | HttpClientErrorException.Forbidden e) { + throw new MarketplaceClientException( + String.format("Access denied when checking project component '%s' in project '%s' and instance '%s'", + componentId, projectId, instanceName), e); + } catch (RestClientException e) { + throw new MarketplaceClientException( + String.format("Failed to check project component '%s' in project '%s' and instance '%s'", + componentId, projectId, instanceName), e); + } + } + + @Override + public boolean provisionProjectComponent(String projectId, List params) throws MarketplaceClientException { + log.debug("Marketpalce service PROVISION component for project {}: ", projectId); + ProvisionAction provisionAction = new ProvisionAction(); + provisionAction.setId("PROVISION"); + provisionAction.addParametersItem(new ProvisionActionParameter().name("project_key").type("string").value(projectId)); + params.forEach(provisionAction::addParametersItem); + + ProvisionerActionsApi provisionerActionsApi = new ProvisionerActionsApi(); + ProvisionActionResponse response = provisionerActionsApi.triggerProvisionAction(provisionAction); + return !response.getFailed(); + } + + @Override + public boolean deleteProjectComponent(String projectId, String componentId) throws MarketplaceClientException { + log.debug("Marketpalce service DELETE component {} for project {}: ", componentId, projectId); + CreateIncidentAction deleteAction = new CreateIncidentAction(); + + ProvisionResultsApi provisionResultsApi = new ProvisionResultsApi(); + ProvisionActionResponse response = provisionResultsApi.createIncident(projectId, componentId, deleteAction); + return !response.getFailed(); + } + + @Override + public void registerProjectComponent(String projectId, String componentId) throws MarketplaceClientException { + log.debug("Marketpalce service REGISTER component {} for project {}: ", componentId, projectId); + NotifyProvisioningStatusUpdateRequest registerRequest = new NotifyProvisioningStatusUpdateRequest(); + registerRequest.setComponentId(componentId); + + ProvisionResultsApi provisionResultsApi = new ProvisionResultsApi(); + provisionResultsApi.notifyProvisioningStatusUpdate(projectId, "CREATED", registerRequest); + } + + /** + * {@inheritDoc} + */ + @Override + public String getDefaultInstance() throws MarketplaceClientException { + return clientFactory.getDefaultInstanceName(); + } + + /** + * {@inheritDoc} + * + * Returns {@code false} (without throwing) if no instances are configured. + */ + @Override + public boolean isHealthy() { + Set instances = getAvailableInstances(); + if (instances.isEmpty()) { + log.warn("No Marketplace instances configured – reporting unhealthy"); + return false; + } + return true; + } + + @Override + public Set getAvailableInstances() { + return clientFactory.getAvailableInstances(); + } + + @Override + public boolean hasInstance(String instanceName) { + return clientFactory.hasInstance(instanceName); + } +} diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/impl/MarketplaceServiceMockImpl.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/impl/MarketplaceServiceMockImpl.java deleted file mode 100644 index 2301a76..0000000 --- a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/impl/MarketplaceServiceMockImpl.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.opendevstack.apiservice.externalservice.marketplace.service.impl; - -import lombok.extern.slf4j.Slf4j; -import org.opendevstack.apiservice.externalservice.marketplace.model.CreateComponentParameter; -import org.opendevstack.apiservice.externalservice.marketplace.model.ProjectComponent; -import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; -import org.springframework.stereotype.Service; - -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -@Service -@Slf4j -public class MarketplaceServiceMockImpl implements MarketplaceService { - - @Override - public boolean isHealthy() { - return true; - } - - private Map mockComponentsCache = new HashMap<>(); - - public ProjectComponent getProjectComponent(String projectId, String componentId) { - log.info("Get component with id '" + componentId + "' for project '" + projectId + "'"); - ComposedId composedId = new ComposedId(projectId, componentId); - return mockComponentsCache.get(composedId); - } - - public ProjectComponent createProjectComponent(String projectId, List createComponentParams) { - log.info("Creating component for project '" + projectId + "'" + " with request: " + createComponentParams); - ProjectComponent mockComponent = new ProjectComponent(); - mockComponent.setComponentId(generateNextId()); - mockComponent.setCanBeDeleted(true); - mockComponent.setStatus("CREATING"); - ComposedId composedId = new ComposedId(projectId, mockComponent.getComponentId()); - mockComponentsCache.put(composedId, mockComponent); - - return mockComponent; - } - - private String generateNextId() { - return "mock-component-id-" + (mockComponentsCache.size() + 1); - } - - class ComposedId { - private String projectId; - private String componentId; - - public ComposedId(String projectId, String componentId) { - this.projectId = projectId; - this.componentId = componentId; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - ComposedId that = (ComposedId) o; - - if (!projectId.equals(that.projectId)) return false; - return componentId.equals(that.componentId); - } - - @Override - public int hashCode() { - int result = projectId.hashCode(); - result = 31 * result + componentId.hashCode(); - return result; - } - } -} From 27682ee04bca29928f9bdd26e0ddd28bda21ef92 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Tue, 14 Apr 2026 15:59:40 +0200 Subject: [PATCH 02/25] Add marketplace api client. --- .../marketplace/client/MarketplaceApiClientFactory.java | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) 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 d5c15bc..b5e2a1f 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 @@ -157,7 +157,7 @@ private RestTemplate createRestTemplate(MarketplaceInstanceConfig config) { restTemplate.setRequestFactory(requestFactory); if (config.isTrustAllCertificates()) { - log.warn("Trust all certificates is enabled for Marketplace connection. " + log.warn("Trust all certificates is enabled for Marketplace API connection. " + "This should only be used in development environments!"); configureTrustAllCertificates(restTemplate); } @@ -171,7 +171,6 @@ private RestTemplate createRestTemplate(MarketplaceInstanceConfig config) { * * @param restTemplate RestTemplate to configure */ - @SuppressWarnings({"java:S4830", "java:S1186"}) private void configureTrustAllCertificates(RestTemplate restTemplate) { try { TrustManager[] trustAllCerts = new TrustManager[]{ @@ -189,7 +188,6 @@ public void checkServerTrusted(X509Certificate[] certs, String authType) { } } }; - SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); @@ -197,8 +195,8 @@ public void checkServerTrusted(X509Certificate[] certs, String authType) { // Intentionally disabling hostname verification for development environments HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true); - } catch (NoSuchAlgorithmException | KeyManagementException e) { - log.error("Failed to configure SSL trust all certificates", e); + } catch (NoSuchAlgorithmException | KeyManagementException ex) { + log.error("Failed to configure SSL trust all certificates for Marketplace API", ex); } } } From 2c9e956a4ec0c45c254d688ce67b3807e1fac3a9 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Tue, 14 Apr 2026 16:15:07 +0200 Subject: [PATCH 03/25] Remove unused var. --- .../apiservice/project/facade/ComponentsFacadeTest.java | 1 - .../marketplace/client/MarketplaceApiClientFactory.java | 7 +++---- 2 files changed, 3 insertions(+), 5 deletions(-) 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 4231ff7..a1b8880 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 @@ -60,7 +60,6 @@ void testGetProjectComponent_whenNoComponentFound_thenReturnNull() throws Except @Test void testCreateProjectComponent_whenSuccess_thenReturnCorrectComponent() throws Exception { - ProjectComponentInfo testComponent = buildTestMarketplaceComponent(); CreateComponentRequest testRequest = buildTestCreateComponentRequest(); when(marketplaceExternalService.provisionProjectComponent(anyString(), any(List.class))) 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 b5e2a1f..d6afc56 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 @@ -159,7 +159,7 @@ private RestTemplate createRestTemplate(MarketplaceInstanceConfig config) { if (config.isTrustAllCertificates()) { log.warn("Trust all certificates is enabled for Marketplace API connection. " + "This should only be used in development environments!"); - configureTrustAllCertificates(restTemplate); + configureTrustAllCertificates(); } return restTemplate; @@ -168,10 +168,9 @@ private RestTemplate createRestTemplate(MarketplaceInstanceConfig config) { /** * Configure RestTemplate to trust all SSL certificates. * WARNING: This should only be used in development environments. - * - * @param restTemplate RestTemplate to configure */ - private void configureTrustAllCertificates(RestTemplate restTemplate) { + @SuppressWarnings({"java:S4830", "java:S1186"}) + private void configureTrustAllCertificates() { try { TrustManager[] trustAllCerts = new TrustManager[]{ new X509TrustManager() { From 6ee3a286357380ce95b36c0a54fdf614ff59e0b2 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Tue, 14 Apr 2026 16:39:20 +0200 Subject: [PATCH 04/25] Fix sonarqube complaints. --- .../client/MarketplaceApiClientFactory.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) 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 d6afc56..5ad3fd7 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 @@ -172,25 +172,22 @@ private RestTemplate createRestTemplate(MarketplaceInstanceConfig config) { @SuppressWarnings({"java:S4830", "java:S1186"}) private void configureTrustAllCertificates() { try { - TrustManager[] trustAllCerts = new TrustManager[]{ + TrustManager[] trustAllCerttificates = new TrustManager[]{ new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } - // Intentionally empty - trusting all certificates for development environments public void checkClientTrusted(X509Certificate[] certs, String authType) { - // No validation performed - development only } // Intentionally empty - trusting all certificates for development environments public void checkServerTrusted(X509Certificate[] certs, String authType) { - // No validation performed - development only } } }; - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustAllCerts, new java.security.SecureRandom()); + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, trustAllCerttificates, new java.security.SecureRandom()); - HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory()); + HttpsURLConnection.setDefaultSSLSocketFactory(context.getSocketFactory()); // Intentionally disabling hostname verification for development environments HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true); From cf25bfbd04f304effd3b91e217095179cd26e426 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Tue, 21 Apr 2026 10:48:37 +0200 Subject: [PATCH 05/25] Add delete component functionality - not finished. --- .../ProjectComponentsController.java | 33 +- .../project/facade/ComponentsFacade.java | 25 +- .../openapi-component_catalog-v1.0.0.yaml | 97 +++-- .../openapi-component_provisioner-v1.0.0.yaml | 4 + .../client/MarketplaceApiClientFactory.java | 22 +- .../config/MarketplaceServiceConfig.java | 5 - .../exception/MarketplaceClientException.java | 12 - .../exception/MarketplaceException.java | 12 + .../service/MarketplaceService.java | 20 +- .../service/impl/MarketplaceServiceImpl.java | 132 +++++-- .../MarketplaceApiClientFactoryTest.java | 160 +++++++++ .../service/MarketplaceServiceImplTest.java | 338 ++++++++++++++++++ 12 files changed, 764 insertions(+), 96 deletions(-) delete mode 100644 external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/exception/MarketplaceClientException.java create mode 100644 external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/exception/MarketplaceException.java create mode 100644 external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClientFactoryTest.java create mode 100644 external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/service/MarketplaceServiceImplTest.java 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 4583364..9d24644 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 @@ -43,7 +43,7 @@ public ResponseEntity createProjectComponent(String pro public ResponseEntity getProjectComponent(String projectId, String componentId) { try { Component component = componentsFacade.getProjectComponent(projectId, componentId); - log.info("Retrieved component '{}' for project '{}': {}", componentId, projectId, component); + log.info("Retrieved component {} for component id '{}' and project '{}'", component, componentId, projectId); if (component == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); } @@ -53,4 +53,35 @@ public ResponseEntity getProjectComponent(String projectId, String co return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } + + @Override + public ResponseEntity registerProjectComponent(String projectId, String componentId) { + try { + boolean success = componentsFacade.registerProjectComponent(projectId, componentId); + log.info("Registered component for project id {} and component id {}", projectId, componentId); + if (!success) { + log.error("Failed to register component for project '{}'", projectId); + return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.error(projectId)); + } + return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.entityCreated(projectId)); + } catch (Exception e) { + log.error("Error while trying to register component for project '" + projectId + "': " + e.getMessage(), e); + return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.error(projectId)); + } + } + + @Override + public ResponseEntity deleteProjectComponent(String projectId, String componentId) { + try { + Boolean result = componentsFacade.deleteProjectComponent(projectId, componentId); + log.info("Delete component '{}' for project '{}' with result {}", componentId, projectId, result); + if (!result) { + return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + } + return ResponseEntity.status(HttpStatus.OK).build(); + } catch (Exception e) { + log.error("Error deleting component '{}' for project '{}': {}", componentId, projectId, e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } } 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 8bfd854..67b3ca6 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,7 +3,7 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; -import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceClientException; +import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; @@ -22,7 +22,7 @@ public class ComponentsFacade { private final MarketplaceMapper marketplaceMapper; - public Component getProjectComponent(String projectId, String componentId) throws MarketplaceClientException { + public Component getProjectComponent(String projectId, String componentId) throws MarketplaceException { ProjectComponentInfo marketplaceComponent = marketplaceExternalService.getProjectComponent(projectId, componentId); if (marketplaceComponent == null) { log.info("Marketplace component with id {} not found", componentId); @@ -31,7 +31,7 @@ public Component getProjectComponent(String projectId, String componentId) throw return marketplaceMapper.mapMarketplaceComponentToV0Component(marketplaceComponent); } - public boolean createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) throws MarketplaceClientException { + public boolean createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) throws MarketplaceException { List createComponentParameterList = marketplaceMapper.mapCreateComponentRequestToCreateComponentParameterList(createComponentRequest); boolean success = marketplaceExternalService.provisionProjectComponent(projectId, createComponentParameterList); if (!success) { @@ -39,4 +39,23 @@ public boolean createProjectComponent(String projectId, CreateComponentRequest c } return success; } + + public Boolean deleteProjectComponent(String projectId, String componentId) { + try { + return marketplaceExternalService.deleteProjectComponent(projectId, componentId); + } catch (MarketplaceException e) { + log.error("Failed to delete component with id {} for project with id {}", componentId, projectId, e); + return false; + } + } + + public boolean registerProjectComponent(String projectId, String componentId) { + try { + marketplaceExternalService.registerProjectComponent(projectId, componentId); + return true; + } catch (MarketplaceException e) { + log.error("Failed to register component in marketplace for project with id {}", projectId, e); + return false; + } + } } diff --git a/external-service-marketplace/openapi/openapi-component_catalog-v1.0.0.yaml b/external-service-marketplace/openapi/openapi-component_catalog-v1.0.0.yaml index bf4b70c..82ca393 100644 --- a/external-service-marketplace/openapi/openapi-component_catalog-v1.0.0.yaml +++ b/external-service-marketplace/openapi/openapi-component_catalog-v1.0.0.yaml @@ -48,12 +48,6 @@ paths: required: true schema: type: string - - name: accessToken - in: query - description: access token for azure queries. - required: true - schema: - type: string responses: "200": description: A list of Project Component Information @@ -162,6 +156,72 @@ paths: application/json: schema: $ref: '#/components/schemas/RestErrorMessage' + /catalog-items/slug/{slug}: + get: + tags: + - CatalogItems + summary: Returns the CatalogItem associated to the provided slug. + description: > + Returns the CatalogItem identified by a composite slug with format `{project-key}_{catalog-item-repository-name}`.
+ The separator is the first underscore (`_`); everything after it (the repo name) may itself contain underscores.
+ The project-key is the normalised (lowercase) Bitbucket project key that owns the item's repository. + The catalog-item-repository-name is matched against the Bitbucket repository slug of the item.
+ Returns 404 if no catalog item matches the provided slug. + operationId: getCatalogItemBySlug + parameters: + - name: slug + in: path + description: > + Composite slug with format `{project-key}_{catalog-item-repository-name}`. + The separator is the first underscore; the repo name may contain additional underscores. + Example: `myproject_my-component-repo` + required: true + schema: + type: string + example: 'myproject_my-component-repo' + responses: + "200": + description: The CatalogItem. + content: + application/json: + schema: + $ref: '#/components/schemas/CatalogItem' + "400": + description: Invalid or malformed slug provided. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "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' + "404": + description: No CatalogItem associated to the provided slug. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "422": + description: Invalid CatalogItem associated to the provided slug. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' /catalog-items: get: tags: @@ -291,12 +351,6 @@ paths: required: true schema: type: string - - name: accessToken - in: query - description: access token for azure queries. - required: true - schema: - type: string - name: sortByTitle in: query description: Sort the returned CatalogItems by title, either in ascending or descending order. @@ -370,12 +424,6 @@ paths: required: true schema: type: string - - name: accessToken - in: query - description: access token for azure queries. - required: true - schema: - type: string responses: "200": description: The CatalogItem. @@ -826,6 +874,12 @@ components: id: type: string example: 'aSdFam...yCg==' + slug: + type: string + description: > + Composite slug computed from the normalised Bitbucket project key and the repository slug of the item, + in the format `{project-key}_{repo-name}`. Calculated at mapping time; not retrieved from Bitbucket. + example: 'myproject_my-component-repo' path: type: string example: projects/SOMEPROJECT/repos/some-repo/raw/CatalogItem.yaml?at=refs/heads/master @@ -875,6 +929,7 @@ components: - date example: id: aSdFam...yCg== + slug: myproject_some-repo path: projects/SOMEPROJECT/repos/some-repo/raw/CatalogItem.yaml?at=refs/heads/master title: An item title shortDescription: This is a short description for the item @@ -1151,12 +1206,6 @@ components: example: "https://bitbucket.com/projects/DEVSTACK/repos/devstack-component-catalog" nullable: true - accessToken: - type: string - description: the access token to be used to get azure groups - example: "some-access-token" - nullable: false - parameters: type: array description: List of name/value string parameters. 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 fb49f6d..b2d079c 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 @@ -172,6 +172,10 @@ paths: 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. 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 5ad3fd7..92ac54e 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 @@ -3,7 +3,7 @@ import lombok.extern.slf4j.Slf4j; import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig; import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig.MarketplaceInstanceConfig; -import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceClientException; +import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; @@ -48,13 +48,13 @@ public MarketplaceApiClientFactory(MarketplaceServiceConfig configuration, *
    *
  • If the default instance is configured via {@code externalservices.marketplace.default-instance}, it is returned.
  • *
  • Otherwise the first entry of the instances map is returned (insertion order).
  • - *
  • If no instances are configured at all, a {@link MarketplaceClientException} is thrown.
  • + *
  • If no instances are configured at all, a {@link MarketplaceException} is thrown.
  • *
* * @return The resolved instance name (never {@code null}/blank) - * @throws MarketplaceClientException if no Marketplace instances are configured + * @throws MarketplaceException if no Marketplace instances are configured */ - public String getDefaultInstanceName() throws MarketplaceClientException { + public String getDefaultInstanceName() throws MarketplaceException { String defaultInstance = configuration.getDefaultInstance(); if (defaultInstance != null && !defaultInstance.isBlank()) { @@ -63,7 +63,7 @@ public String getDefaultInstanceName() throws MarketplaceClientException { Map instances = configuration.getInstances(); if (instances == null || instances.isEmpty()) { - throw new MarketplaceClientException("No Marketplace instances configured"); + throw new MarketplaceException("No Marketplace instances configured"); } return instances.keySet().iterator().next(); @@ -75,12 +75,12 @@ public String getDefaultInstanceName() throws MarketplaceClientException { * * @param instanceName Name of the Marketplace instance, or {@code null}/{@code ""} for the default * @return Configured MarketplaceApiClient - * @throws MarketplaceClientException if the instance is not configured + * @throws MarketplaceException if the instance is not configured */ @Cacheable(value = "marketplaceApiClients", key = "#instanceName", condition = "#instanceName != null && !#instanceName.isBlank()") - public MarketplaceApiClient getClient(String instanceName) throws MarketplaceClientException { + public MarketplaceApiClient getClient(String instanceName) throws MarketplaceException { if (instanceName == null || instanceName.isBlank()) { - throw new MarketplaceClientException( + throw new MarketplaceException( String.format("Provide instance name. Available instances: %s", configuration.getInstances().keySet())); } @@ -88,7 +88,7 @@ public MarketplaceApiClient getClient(String instanceName) throws MarketplaceCli MarketplaceInstanceConfig instanceConfig = configuration.getInstances().get(instanceName); if (instanceConfig == null) { - throw new MarketplaceClientException( + throw new MarketplaceException( String.format("Marketplace instance '%s' is not configured. Available instances: %s", instanceName, configuration.getInstances().keySet())); } @@ -104,10 +104,10 @@ public MarketplaceApiClient getClient(String instanceName) throws MarketplaceCli * Falls back to the first configured instance when {@code default-instance} is not set. * * @return MarketplaceApiClient for the default instance - * @throws MarketplaceClientException if no instances are configured + * @throws MarketplaceException if no instances are configured */ @Cacheable(value = "marketplaceApiClients", key = "'default'") - public MarketplaceApiClient getClient() throws MarketplaceClientException { + public MarketplaceApiClient getClient() throws MarketplaceException { String defaultInstanceName = getDefaultInstanceName(); MarketplaceInstanceConfig instanceConfig = configuration.getInstances().get(defaultInstanceName); RestTemplate restTemplate = createRestTemplate(instanceConfig); diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceServiceConfig.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceServiceConfig.java index ad7adce..2a0f593 100644 --- a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceServiceConfig.java +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceServiceConfig.java @@ -39,11 +39,6 @@ public static class MarketplaceInstanceConfig { */ private String provisionerActionsBaseUrl; - /** - * The provision results base URL of the Marketplace - */ - private String provisionResultsBaseUrl; - /** * Authentication access token for accessing the Marketplace API */ diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/exception/MarketplaceClientException.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/exception/MarketplaceClientException.java deleted file mode 100644 index 4a74d25..0000000 --- a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/exception/MarketplaceClientException.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.opendevstack.apiservice.externalservice.marketplace.exception; - -public class MarketplaceClientException extends Exception { - - public MarketplaceClientException(String message) { - super(message); - } - - public MarketplaceClientException(String message, Throwable cause) { - super(message, cause); - } -} diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/exception/MarketplaceException.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/exception/MarketplaceException.java new file mode 100644 index 0000000..f31934c --- /dev/null +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/exception/MarketplaceException.java @@ -0,0 +1,12 @@ +package org.opendevstack.apiservice.externalservice.marketplace.exception; + +public class MarketplaceException extends Exception { + + public MarketplaceException(String message) { + super(message); + } + + public MarketplaceException(String message, Throwable cause) { + super(message, cause); + } +} 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 eeb0d75..28f74f8 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 @@ -1,7 +1,7 @@ package org.opendevstack.apiservice.externalservice.marketplace.service; import org.opendevstack.apiservice.externalservice.api.ExternalService; -import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceClientException; +import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; @@ -14,16 +14,22 @@ public interface MarketplaceService extends ExternalService { boolean hasInstance(String instanceName); - String getDefaultInstance() throws MarketplaceClientException; + String getDefaultInstance() throws MarketplaceException; - ProjectComponentInfo getProjectComponent(String projectId, String componentId) throws MarketplaceClientException; + ProjectComponentInfo getProjectComponent(String projectId, String componentId) throws MarketplaceException; - ProjectComponentInfo getProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceClientException; + ProjectComponentInfo getProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException; - boolean provisionProjectComponent(String projectId, List params) throws MarketplaceClientException; + boolean provisionProjectComponent(String projectId, List params) throws MarketplaceException; - boolean deleteProjectComponent(String projectId, String componentId) throws MarketplaceClientException; + boolean provisionProjectComponent(String instanceName, String projectId, List params) throws MarketplaceException; - void registerProjectComponent(String projectId, String componentId) throws MarketplaceClientException; + boolean deleteProjectComponent(String projectId, String componentId) throws MarketplaceException; + + boolean deleteProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException; + + void registerProjectComponent(String projectId, String componentId) throws MarketplaceException; + + void registerProjectComponent(String instanceName, 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 6176415..ac9b526 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 @@ -3,7 +3,7 @@ import lombok.extern.slf4j.Slf4j; import org.opendevstack.apiservice.externalservice.marketplace.client.MarketplaceApiClient; import org.opendevstack.apiservice.externalservice.marketplace.client.MarketplaceApiClientFactory; -import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceClientException; +import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; import org.opendevstack.apiservice.externalservice.marketplace.openapi.ApiClient; import org.opendevstack.apiservice.externalservice.marketplace.openapi.api.ProjectComponentsApi; import org.opendevstack.apiservice.externalservice.marketplace.openapi.api.ProvisionResultsApi; @@ -34,73 +34,139 @@ public MarketplaceServiceImpl(MarketplaceApiClientFactory clientFactory) { } @Override - public ProjectComponentInfo getProjectComponent(String projectId, String componentId) throws MarketplaceClientException { + public ProjectComponentInfo getProjectComponent(String projectId, String componentId) throws MarketplaceException { return getProjectComponent(getDefaultInstance(), projectId, componentId); } @Override - public ProjectComponentInfo getProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceClientException { + public ProjectComponentInfo getProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException { log.debug("Marketplace service GET component with id {} for project {} in instance {} ", componentId, projectId, instanceName); try { MarketplaceApiClient marketplaceClient = clientFactory.getClient(instanceName); ApiClient apiClient = marketplaceClient.getApiClient(); - ProjectComponentsApi projectComponentsApi = new ProjectComponentsApi(apiClient); apiClient.setBasePath(marketplaceClient.getConfig().getProjectComponentsBaseUrl()); - - return projectComponentsApi.getProjectComponents(projectId, marketplaceClient.getConfig().getAccessToken()).stream() + List components = projectComponentsApi.getProjectComponents(projectId); + if (components == null || components.isEmpty()) { + return null; + } + return components.stream() .filter(component -> component.getComponentId().equals(componentId)) .findFirst() .orElse(null); + } catch (HttpClientErrorException.NotFound e) { + log.debug("Component with id '{}' not found in Marketplace instance '{}' for project '{}'", + componentId, instanceName, projectId); + return null; } catch (HttpClientErrorException.Unauthorized | HttpClientErrorException.Forbidden e) { - throw new MarketplaceClientException( - String.format("Access denied when checking project component '%s' in project '%s' and instance '%s'", + throw new MarketplaceException( + String.format("Access denied when getting project component '%s' in project '%s' and instance '%s'", componentId, projectId, instanceName), e); } catch (RestClientException e) { - throw new MarketplaceClientException( - String.format("Failed to check project component '%s' in project '%s' and instance '%s'", + throw new MarketplaceException( + String.format("Failed to retrieve project component '%s' in project '%s' and instance '%s'", componentId, projectId, instanceName), e); } } @Override - public boolean provisionProjectComponent(String projectId, List params) throws MarketplaceClientException { - log.debug("Marketpalce service PROVISION component for project {}: ", projectId); - ProvisionAction provisionAction = new ProvisionAction(); - provisionAction.setId("PROVISION"); - provisionAction.addParametersItem(new ProvisionActionParameter().name("project_key").type("string").value(projectId)); - params.forEach(provisionAction::addParametersItem); - - ProvisionerActionsApi provisionerActionsApi = new ProvisionerActionsApi(); - ProvisionActionResponse response = provisionerActionsApi.triggerProvisionAction(provisionAction); - return !response.getFailed(); + public boolean provisionProjectComponent(String projectId, List params) throws MarketplaceException { + return provisionProjectComponent(getDefaultInstance(), projectId, params); } @Override - public boolean deleteProjectComponent(String projectId, String componentId) throws MarketplaceClientException { - log.debug("Marketpalce service DELETE component {} for project {}: ", componentId, projectId); - CreateIncidentAction deleteAction = new CreateIncidentAction(); + public boolean provisionProjectComponent(String instanceName, String projectId, List params) throws MarketplaceException { + log.debug("Marketplace service PROVISION component for project {}: ", projectId); + try { + MarketplaceApiClient marketplaceClient = clientFactory.getClient(instanceName); + ApiClient apiClient = marketplaceClient.getApiClient(); + + ProvisionAction provisionAction = new ProvisionAction(); + provisionAction.setId("PROVISION"); + provisionAction.addParametersItem(new ProvisionActionParameter().name("project_key").type("string").value(projectId)); + params.forEach(provisionAction::addParametersItem); + + ProvisionerActionsApi provisionerActionsApi = new ProvisionerActionsApi(apiClient); + apiClient.setBasePath(marketplaceClient.getConfig().getProvisionerActionsBaseUrl()); + + ProvisionActionResponse response = provisionerActionsApi.triggerProvisionAction(provisionAction); + return !response.getFailed(); + } catch (HttpClientErrorException.Unauthorized | HttpClientErrorException.Forbidden e) { + throw new MarketplaceException( + String.format("Access denied when provisioning project component in project '%s' and instance '%s'", + projectId, instanceName), e); + } catch (RestClientException e) { + throw new MarketplaceException( + String.format("Failed to provision project component in project '%s' and instance '%s'", + projectId, instanceName), e); + } + } + + @Override + public boolean deleteProjectComponent(String projectId, String componentId) throws MarketplaceException { + return deleteProjectComponent(getDefaultInstance(), projectId, componentId); + } + + @Override + public boolean deleteProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException { + log.debug("Marketplace service DELETE component {} for project {}: ", componentId, projectId); + try { + 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()); - ProvisionResultsApi provisionResultsApi = new ProvisionResultsApi(); - ProvisionActionResponse response = provisionResultsApi.createIncident(projectId, componentId, deleteAction); - return !response.getFailed(); + CreateIncidentAction deleteAction = new CreateIncidentAction(); + ProvisionActionResponse response = provisionResultsApi.createIncident(projectId, componentId, deleteAction); + return !response.getFailed(); + } catch (HttpClientErrorException.Unauthorized | HttpClientErrorException.Forbidden e) { + throw new MarketplaceException( + String.format("Access denied when deleting project component '%s' in project '%s' and instance '%s'", + componentId, projectId, instanceName), e); + } catch (RestClientException e) { + throw new MarketplaceException( + String.format("Failed to delete project component '%s' in project '%s' and instance '%s'", + componentId, projectId, instanceName), e); + } } @Override - public void registerProjectComponent(String projectId, String componentId) throws MarketplaceClientException { - log.debug("Marketpalce service REGISTER component {} for project {}: ", componentId, projectId); - NotifyProvisioningStatusUpdateRequest registerRequest = new NotifyProvisioningStatusUpdateRequest(); - registerRequest.setComponentId(componentId); + public void registerProjectComponent(String projectId, String componentId) throws MarketplaceException { + registerProjectComponent(getDefaultInstance(), projectId, componentId); + } - ProvisionResultsApi provisionResultsApi = new ProvisionResultsApi(); - provisionResultsApi.notifyProvisioningStatusUpdate(projectId, "CREATED", registerRequest); + @Override + public void registerProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException { + log.debug("Marketplace service REGISTER component {} for project {}: ", componentId, projectId); + try { + 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); + provisionResultsApi.notifyProvisioningStatusUpdate(projectId, "CREATED", registerRequest); + } catch (HttpClientErrorException.Unauthorized | HttpClientErrorException.Forbidden e) { + throw new MarketplaceException( + String.format("Access denied when registering project component '%s' in project '%s' and instance '%s'", + componentId, projectId, instanceName), e); + } catch (RestClientException e) { + throw new MarketplaceException( + String.format("Failed to register project component '%s' in project '%s' and instance '%s'", + componentId, projectId, instanceName), e); + } } /** * {@inheritDoc} */ @Override - public String getDefaultInstance() throws MarketplaceClientException { + public String getDefaultInstance() throws MarketplaceException { return clientFactory.getDefaultInstanceName(); } diff --git a/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClientFactoryTest.java b/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClientFactoryTest.java new file mode 100644 index 0000000..ecb32a5 --- /dev/null +++ b/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClientFactoryTest.java @@ -0,0 +1,160 @@ +package org.opendevstack.apiservice.externalservice.marketplace.client; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig; +import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig.MarketplaceInstanceConfig; +import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.web.client.RestTemplate; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link MarketplaceApiClientFactory}. + * Focuses on the default-instance resolution logic introduced in {@code resolveInstanceName}. + */ +@ExtendWith(MockitoExtension.class) +class MarketplaceApiClientFactoryTest { + + @Mock + private RestTemplateBuilder restTemplateBuilder; + + @Mock + private RestTemplate restTemplate; + + private MarketplaceServiceConfig configuration; + + @BeforeEach + void setUp() { + configuration = new MarketplaceServiceConfig(); + lenient().when(restTemplateBuilder.build()).thenReturn(restTemplate); + } + + private MarketplaceApiClientFactory factory() { + return new MarketplaceApiClientFactory(configuration, restTemplateBuilder); + } + + // ------------------------------------------------------------------------- + // resolveInstanceName → configured default + // ------------------------------------------------------------------------- + + @Test + void resolveInstanceName_null_returnsConfiguredDefaultInstance() throws MarketplaceException { + configuration.setDefaultInstance("prod"); + + assertEquals("prod", factory().getDefaultInstanceName()); + } + + // ------------------------------------------------------------------------- + // resolveInstanceName – without default → fallback to first instance + // ------------------------------------------------------------------------- + + @Test + void resolveInstanceName_null_noDefaultConfigured_returnsFirstInstance() throws MarketplaceException { + // LinkedHashMap preserves insertion order → "alpha" is first + Map instances = new LinkedHashMap<>(); + instances.put("alpha", config("https://marketplace.example.com")); + instances.put("beta", config("https://marketplace-beta.example.com")); + configuration.setInstances(instances); + + assertEquals("alpha", factory().getDefaultInstanceName()); + } + + // ------------------------------------------------------------------------- + // resolveInstanceName – no instances at all → exception + // ------------------------------------------------------------------------- + + @Test + void resolveInstanceName_null_noInstancesConfigured_throwsMarketplaceException() { + // no instances set → empty map + MarketplaceApiClientFactory f = factory(); + + MarketplaceException ex = assertThrows(MarketplaceException.class, () -> f.getDefaultInstanceName()); + assertTrue(ex.getMessage().toLowerCase().contains("no marketplace instances configured"), + "Expected 'no marketplace instances configured' in: " + ex.getMessage()); + } + + @Test + void getClient_null_throwsMarketplaceException() throws MarketplaceException { + MarketplaceException ex = assertThrows(MarketplaceException.class, () -> factory().getClient(null)); + assertTrue(ex.getMessage().toLowerCase().contains("provide instance name"), + "Expected 'provide instance name' in: " + ex.getMessage()); + } + + @Test + void getClient_blank_throwsMarketplaceException() throws MarketplaceException { + MarketplaceException ex = assertThrows(MarketplaceException.class, () -> factory().getClient("")); + assertTrue(ex.getMessage().toLowerCase().contains("provide instance name"), + "Expected 'provide instance name' in: " + ex.getMessage()); + } + + @Test + void getClient_unknownInstance_throwsMarketplaceException() { + configuration.setInstances(Map.of("dev", config("https://marketplace.dev.example.com"))); + + MarketplaceException ex = assertThrows(MarketplaceException.class, + () -> factory().getClient("nonexistent")); + assertTrue(ex.getMessage().contains("not configured")); + assertTrue(ex.getMessage().contains("nonexistent")); + } + + @Test + void getClient_returnsClientForConfiguredDefaultInstance() throws MarketplaceException { + when(restTemplateBuilder.build()).thenReturn(restTemplate); + configuration.setDefaultInstance("prod"); + configuration.setInstances(orderedMap("dev", "prod")); + + MarketplaceApiClient client = factory().getClient(); + + assertNotNull(client); + assertEquals("prod", client.getInstanceName()); + } + + @Test + void getClient_noDefaultConfigured_returnsFirstInstance() throws MarketplaceException { + when(restTemplateBuilder.build()).thenReturn(restTemplate); + Map instances = new LinkedHashMap<>(); + instances.put("alpha", config("https://marketplace.example.com")); + instances.put("beta", config("https://marketplace-beta.example.com")); + configuration.setInstances(instances); + + MarketplaceApiClient client = factory().getClient(); + + assertNotNull(client); + assertEquals("alpha", client.getInstanceName()); + } + + @Test + void getClient_noInstancesConfigured_throwsMarketplaceException() { + MarketplaceException ex = assertThrows(MarketplaceException.class, () -> factory().getClient()); + assertTrue(ex.getMessage().toLowerCase().contains("no marketplace instances configured")); + } + + // ------------------------------------------------------------------------- + // Helpers + // ------------------------------------------------------------------------- + + private static MarketplaceInstanceConfig config(String baseUrl) { + MarketplaceInstanceConfig c = new MarketplaceInstanceConfig(); + c.setProjectComponentsBaseUrl(baseUrl); + c.setProvisionerActionsBaseUrl(baseUrl); + return c; + } + + /** Creates a LinkedHashMap with two configs using their names as base-url stems. */ + private static Map orderedMap(String first, String second) { + Map m = new LinkedHashMap<>(); + m.put(first, config("https://" + first + ".example.com")); + m.put(second, config("https://" + second + ".example.com")); + return m; + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..7ab1151 --- /dev/null +++ b/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/service/MarketplaceServiceImplTest.java @@ -0,0 +1,338 @@ +package org.opendevstack.apiservice.externalservice.marketplace.service; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opendevstack.apiservice.externalservice.marketplace.client.MarketplaceApiClient; +import org.opendevstack.apiservice.externalservice.marketplace.client.MarketplaceApiClientFactory; +import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig.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.ProjectComponentInfo; +import org.opendevstack.apiservice.externalservice.marketplace.service.impl.MarketplaceServiceImpl; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestClientException; + +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link MarketplaceService}. + * These tests use mocks and do not require actual Marketplace connectivity. + */ +@ExtendWith(MockitoExtension.class) +class MarketplaceServiceImplTest { + + // TODO tests for the rest of the methods in MarketplaceServiceImpl + + @Mock + private MarketplaceApiClientFactory clientFactory; + + @Mock + private MarketplaceApiClient marketplaceApiClient; + + @Mock + private ApiClient apiClient; + + private MarketplaceService marketplaceService; + + @BeforeEach + void setUp() { + marketplaceService = new MarketplaceServiceImpl(clientFactory); + // Stub ApiClient utility methods used by the generated ProjectApi / ServerInfoApi + // before invokeAPI is reached. Without these, putAll(null) causes NullPointerException. + lenient().when(apiClient.parameterToMultiValueMap(any(), anyString(), any())) + .thenReturn(new LinkedMultiValueMap<>()); + lenient().when(apiClient.selectHeaderAccept(any())) + .thenReturn(List.of(MediaType.APPLICATION_JSON)); + lenient().when(apiClient.selectHeaderContentType(any())) + .thenReturn(MediaType.APPLICATION_JSON); + } + + // ------------------------------------------------------------------------- + // getProjectComponent + // ------------------------------------------------------------------------- + + @Test + void testGetProjectComponent_InstanceNotConfigured() throws MarketplaceException { + // Arrange + String instanceName = "nonexistent"; + String projectKey = "PROJ"; + String componentId = "test-component"; + + when(clientFactory.getClient(instanceName)) + .thenThrow(new MarketplaceException("Marketplace instance 'nonexistent' is not configured")); + + // Act & Assert + MarketplaceException exception = assertThrows(MarketplaceException.class, () -> + marketplaceService.getProjectComponent(instanceName, projectKey, componentId)); + + assertTrue(exception.getMessage().contains("not configured")); + verify(clientFactory).getClient(instanceName); + } + + @Test + void testGetProjectComponent_RestClientException() throws MarketplaceException { + // Arrange + String instanceName = "dev"; + String projectKey = "PROJ"; + String componentId = "test-component"; + MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); + + when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); + when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); + when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); + + // Act & Assert + assertThrows(MarketplaceException.class, () -> + marketplaceService.getProjectComponent(instanceName, projectKey, componentId)); + + verify(clientFactory).getClient(instanceName); + verify(marketplaceApiClient).getApiClient(); + } + + @Test + void testGetProjectComponent_NotFound_ReturnsNull() throws MarketplaceException { + // Arrange + String instanceName = "dev"; + String projectKey = "UNKNOWN"; + String componentId = "test-component"; + MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); + instanceConfig.setAccessToken("1234"); + HttpClientErrorException notFoundEx = HttpClientErrorException.create( + HttpStatus.NOT_FOUND, "Not Found", HttpHeaders.EMPTY, new byte[0], null); + + 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(notFoundEx); + when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); + + // Act + ProjectComponentInfo result = marketplaceService.getProjectComponent(instanceName, projectKey, componentId); + + // Assert + assertNull(result); + verify(clientFactory).getClient(instanceName); + } + + // ------------------------------------------------------------------------- + // isHealthy + // ------------------------------------------------------------------------- + + @Test + void testIsHealthy_NoInstancesConfigured_ReturnsFalse() { + // Arrange + when(clientFactory.getAvailableInstances()).thenReturn(Collections.emptySet()); + + // Act + boolean result = marketplaceService.isHealthy(); + + // Assert + assertFalse(result); + } + + // TODO reenable these tests when we implement the health check to actually call the Marketplace API. + // For now, since isHealthy only checks if instances are configured, this test is not relevant + // and fails due to the RestClientException being thrown by the mocked ApiClient when invokeAPI is called. +// @Test +// void testIsHealthy_RestClientException_ReturnsFalse() throws MarketplaceException { +// // Arrange +// when(clientFactory.getAvailableInstances()).thenReturn(Set.of("dev")); +// when(clientFactory.getClient()).thenReturn(marketplaceApiClient); +// when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); +// when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) +// .thenThrow(new RestClientException("Connection refused")); +// +// // Act +// boolean result = marketplaceService.isHealthy(); +// +// // Assert +// assertFalse(result); +// } +// +// @Test +// void testIsHealthy_WhenException_ReturnsFalse() throws MarketplaceException { +// // Arrange +// when(clientFactory.getAvailableInstances()).thenReturn(Set.of("dev")); +// when(clientFactory.getClient()).thenThrow(new MarketplaceException("No Marketplace instances configured")); +// +// // Act +// boolean result = marketplaceService.isHealthy(); +// +// // Assert +// assertFalse(result); +// } + + // ------------------------------------------------------------------------- + // getAvailableInstances / hasInstance + // ------------------------------------------------------------------------- + + @Test + void testGetAvailableInstances() { + // Arrange + Set expected = Set.of("dev", "prod"); + when(clientFactory.getAvailableInstances()).thenReturn(expected); + + // Act + Set result = marketplaceService.getAvailableInstances(); + + // Assert + assertEquals(expected, result); + verify(clientFactory).getAvailableInstances(); + } + + @Test + void testHasInstance_Existing_ReturnsTrue() { + // Arrange + when(clientFactory.hasInstance("dev")).thenReturn(true); + + // Act + Assert + assertTrue(marketplaceService.hasInstance("dev")); + } + + @Test + void testHasInstance_NonExistent_ReturnsFalse() { + // Arrange + when(clientFactory.hasInstance("nope")).thenReturn(false); + + // Act + Assert + assertFalse(marketplaceService.hasInstance("nope")); + } + + // ------------------------------------------------------------------------- + // Default-instance support + // ------------------------------------------------------------------------- + + @Test + void testGetProjectComponent_NoInstanceArg_UsesDefaultClient() throws MarketplaceException { + String projectKey = "PROJ"; + String componentId = "test-component"; + MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); + instanceConfig.setAccessToken("1234"); + + when(clientFactory.getClient(null)).thenReturn(marketplaceApiClient); + when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); + when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(ResponseEntity.ok(null)); + when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); + + ProjectComponentInfo result = marketplaceService.getProjectComponent(projectKey, componentId); + + assertNull(result); + verify(clientFactory).getClient(null); + } + + @Test + void testGetProjectComponent_NullInstanceName_UsesDefaultClient() throws MarketplaceException { + // Passing null explicitly as instanceName should also resolve to the default + String projectKey = "PROJ"; + String componentId = "test-component"; + MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); + instanceConfig.setAccessToken("1234"); + + when(clientFactory.getClient(null)).thenReturn(marketplaceApiClient); + when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); + when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(ResponseEntity.ok(null)); + when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); + + ProjectComponentInfo result = marketplaceService.getProjectComponent(null, projectKey, componentId); + + assertNull(result); + verify(clientFactory).getClient(null); + } + + @Test + void testGetProjectComponent_BlankInstanceName_UsesDefaultClient() throws MarketplaceException { + String projectKey = "PROJ"; + String componentId = "test-component"; + MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); + instanceConfig.setAccessToken("1234"); + + when(clientFactory.getClient("")).thenReturn(marketplaceApiClient); + when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); + when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenReturn(ResponseEntity.ok(null)); + when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); + + ProjectComponentInfo result = marketplaceService.getProjectComponent("", projectKey, componentId); + + assertNull(result); + verify(clientFactory).getClient(""); + } + + @Test + void testGetProjectComponent_NoInstanceArg_NotFound_ReturnsFalse() throws MarketplaceException { + String projectKey = "ZZZNOPE"; + String componentId = "test-component"; + MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); + instanceConfig.setAccessToken("1234"); + + HttpClientErrorException notFoundEx = HttpClientErrorException.create( + HttpStatus.NOT_FOUND, "Not Found", HttpHeaders.EMPTY, new byte[0], null); + + when(clientFactory.getClient(null)).thenReturn(marketplaceApiClient); + when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); + when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenThrow(notFoundEx); + when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); + + ProjectComponentInfo result = marketplaceService.getProjectComponent(projectKey, componentId); + + assertNull(result); + } + + @Test + void testGetProjectComponent_NoInstanceArg_RestClientException_ThrowsMarketplaceException() throws MarketplaceException { + MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); + instanceConfig.setAccessToken("1234"); + + when(clientFactory.getClient(null)).thenReturn(marketplaceApiClient); + when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); + when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenThrow(new RestClientException("timeout")); + when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); + + assertThrows(MarketplaceException.class, () -> marketplaceService.getProjectComponent("PROJ", + "test-component")); + } + + @Test + void testGetDefaultInstance_DelegatesToFactory() throws MarketplaceException { + when(clientFactory.getDefaultInstanceName()).thenReturn("prod"); + + String result = marketplaceService.getDefaultInstance(); + + assertEquals("prod", result); + verify(clientFactory).getDefaultInstanceName(); + } + + @Test + void testGetDefaultInstance_FactoryThrows_PropagatesException() throws MarketplaceException { + when(clientFactory.getDefaultInstanceName()) + .thenThrow(new MarketplaceException("No Marketplace instances configured")); + + assertThrows(MarketplaceException.class, () -> marketplaceService.getDefaultInstance()); + } + +} \ No newline at end of file From 0e72bc98d8f75de99233cf1292d868ba56344a66 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Tue, 21 Apr 2026 13:00:35 +0200 Subject: [PATCH 06/25] Add delete component functionality - not finished. --- .../ProjectComponentsController.java | 71 ++++++++----------- .../exception/ComponentCreationException.java | 4 ++ .../exception/ComponentNotFoundException.java | 4 ++ .../project/facade/ComponentsFacade.java | 22 +++--- .../project/mapper/MarketplaceMapper.java | 16 ++--- .../ProjectComponentsControllerTest.java | 23 +++--- .../project/facade/ComponentsFacadeTest.java | 40 ++++++----- .../project/util/TestObjectsBuilder.java | 7 +- .../service/MarketplaceServiceImplTest.java | 2 + 9 files changed, 98 insertions(+), 91 deletions(-) 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 366a889..e48c6f7 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 @@ -2,6 +2,7 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; import org.opendevstack.apiservice.project.api.ProjectComponentsApi; import org.opendevstack.apiservice.project.exception.ComponentCreationException; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; @@ -31,58 +32,44 @@ public class ProjectComponentsController implements ProjectComponentsApi { @Override public ResponseEntity createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { - Component component = componentsFacade.createProjectComponent(projectId, createComponentRequest); - if (component == null) { - throw new ComponentCreationException(String.format("Failed to create component for project '%s'", projectId)); - } - - log.info("Created component {} for project id {} and request {}", component, projectId, createComponentRequest); - return componentResponseMapper.toResponseEntity( - ComponentsResponseFactory.entityCreated(projectId, component.getId()) - ); - } + try { + Component component = componentsFacade.provisionProjectComponent(projectId, createComponentRequest); + if (component == null) { + throw new ComponentCreationException(String.format("Failed to create component for project '%s'", projectId)); + } - @Override - public ResponseEntity getProjectComponent(String projectId, UUID componentId) { - Component component = componentsFacade.getProjectComponent(projectId, componentId.toString()); - if (component == null) { - throw new ComponentNotFoundException( - String.format("Component '%s' not found for project '%s'", componentId, projectId) + log.info("Created component {} for project id {} and request {}", component, projectId, createComponentRequest); + return componentResponseMapper.toResponseEntity( + ComponentsResponseFactory.entityCreated(projectId, component.getId()) + ); + } catch (MarketplaceException e) { //TODO use error handler + log.error("Error while creating component for project '{}': {}", projectId, e.getMessage(), e); + throw new ComponentCreationException( + String.format("Failed to create component for project '%s': %s", projectId, e.getMessage()), e ); } - log.info("Retrieved component '{}' for project '{}': {}", componentId, projectId, component); - return ResponseEntity.status(HttpStatus.OK).body(component); } @Override - public ResponseEntity registerProjectComponent(String projectId, String componentId) { + public ResponseEntity getProjectComponent(String projectId, UUID componentId) { try { - boolean success = componentsFacade.registerProjectComponent(projectId, componentId); - log.info("Registered component for project id {} and component id {}", projectId, componentId); - if (!success) { - log.error("Failed to register component for project '{}'", projectId); - return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.error(projectId)); - } - return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.entityCreated(projectId)); - } catch (Exception e) { - log.error("Error while trying to register component for project '" + projectId + "': " + e.getMessage(), e); - return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.error(projectId)); - } - } - @Override - public ResponseEntity deleteProjectComponent(String projectId, String componentId) { - try { - Boolean result = componentsFacade.deleteProjectComponent(projectId, componentId); - log.info("Delete component '{}' for project '{}' with result {}", componentId, projectId, result); - if (!result) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); + Component component = componentsFacade.getProjectComponent(projectId, componentId.toString()); + if (component == null) { + throw new ComponentNotFoundException( + String.format("Component '%s' not found for project '%s'", componentId, projectId) + ); } - return ResponseEntity.status(HttpStatus.OK).build(); - } catch (Exception e) { - log.error("Error deleting component '{}' for project '{}': {}", componentId, projectId, e.getMessage(), e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + + log.info("Retrieved component '{}' for project '{}': {}", componentId, projectId, component); + return ResponseEntity.status(HttpStatus.OK).body(component); + } catch (MarketplaceException e) { //TODO use error handler + log.error("Error while retrieving component '{}' for project '{}': {}", componentId, projectId, e.getMessage(), e); + throw new ComponentNotFoundException( + String.format("Failed to retrieve 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/exception/ComponentCreationException.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentCreationException.java index f035918..dce8484 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentCreationException.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentCreationException.java @@ -5,4 +5,8 @@ public class ComponentCreationException extends RuntimeException { public ComponentCreationException(String message) { super(message); } + + public ComponentCreationException(String message, Exception e) { + super(message, e); + } } diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentNotFoundException.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentNotFoundException.java index 8ee5ffc..38caa87 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentNotFoundException.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentNotFoundException.java @@ -5,4 +5,8 @@ public class ComponentNotFoundException extends RuntimeException { public ComponentNotFoundException(String message) { super(message); } + + public ComponentNotFoundException(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 2577014..96849e0 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 @@ -2,9 +2,12 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.opendevstack.apiservice.externalservice.marketplace.model.CreateComponentParameter; -import org.opendevstack.apiservice.externalservice.marketplace.model.ProjectComponent; +import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; +import org.opendevstack.apiservice.project.exception.ComponentCreationException; +import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.CreateComponentRequest; @@ -21,8 +24,8 @@ public class ComponentsFacade { private final MarketplaceMapper marketplaceMapper; - public Component getProjectComponent(String projectId, String componentId) { - ProjectComponent marketplaceComponent = marketplaceExternalService.getProjectComponent(projectId, componentId); + public Component getProjectComponent(String projectId, String componentId) throws MarketplaceException { + ProjectComponentInfo marketplaceComponent = marketplaceExternalService.getProjectComponent(projectId, componentId); if (marketplaceComponent == null) { log.info("Marketplace component with id {} not found", componentId); throw new ComponentNotFoundException( @@ -32,16 +35,17 @@ public Component getProjectComponent(String projectId, String componentId) { return marketplaceMapper.mapMarketplaceComponentToV0Component(marketplaceComponent); } - public Component createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { - List createComponentParameterList = marketplaceMapper.mapCreateComponentRequestToCreateComponentParameterList(createComponentRequest); - ProjectComponent marketplaceComponent = marketplaceExternalService.createProjectComponent(projectId, createComponentParameterList); - if (marketplaceComponent == null) { + public Component provisionProjectComponent(String projectId, CreateComponentRequest createComponentRequest) throws MarketplaceException { + List createComponentParameterList = marketplaceMapper.mapCreateComponentRequestToCreateComponentParameterList(createComponentRequest); + //TODO implement this with more info from marketplace + boolean success = marketplaceExternalService.provisionProjectComponent(projectId, createComponentParameterList); + if (!success) { log.error("Failed to create component in marketplace for project with id {}", projectId); throw new ComponentCreationException( String.format("Failed to create component for project '%s'", projectId) ); } - return marketplaceMapper.mapMarketplaceComponentToV0Component(marketplaceComponent); + return marketplaceMapper.mapMarketplaceComponentToV0Component(new ProjectComponentInfo()); //TODO get the created component info from marketplace and map it } public Boolean deleteProjectComponent(String projectId, String componentId) { 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 8e84500..5119396 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 @@ -4,8 +4,8 @@ import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Named; -import org.opendevstack.apiservice.externalservice.marketplace.model.CreateComponentParameter; -import org.opendevstack.apiservice.externalservice.marketplace.model.ProjectComponent; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.ComponentsStatusDTO; import org.opendevstack.apiservice.project.model.CreateComponentRequest; @@ -19,13 +19,13 @@ public interface MarketplaceMapper { @Mapping(target = "id", source = "componentId", qualifiedByName = "uuidToString") - @Mapping(target = "environment", source = "environment", qualifiedByName = "toEnvironment") +// TODO @Mapping(target = "environment", source = "environment", qualifiedByName = "toEnvironment") @Mapping(target = "status", source = "status", qualifiedByName = "toComponentStatus") - @Mapping(target = "params", expression = "java(java.util.Collections.emptyMap())") +// TODO @Mapping(target = "params", expression = "java(java.util.Collections.emptyMap())") @Mapping(target = "resultTraceback", ignore = true) - Component mapMarketplaceComponentToV0Component(ProjectComponent source); + Component mapMarketplaceComponentToV0Component(ProjectComponentInfo source); - default List mapCreateComponentRequestToCreateComponentParameterList(CreateComponentRequest createComponentRequest) { + default List mapCreateComponentRequestToCreateComponentParameterList(CreateComponentRequest createComponentRequest) { if (createComponentRequest == null || createComponentRequest.getParams() == null) { return List.of(); } @@ -34,13 +34,13 @@ default List mapCreateComponentRequestToCreateComponen } @IterableMapping(qualifiedByName = "toCreateComponentParameter") - List mapEntriesToCreateComponentParameterList(List> entries); + List mapEntriesToCreateComponentParameterList(List> entries); @Named("toCreateComponentParameter") @Mapping(target = "name", source = "key") @Mapping(target = "type", constant = "string") @Mapping(target = "value", expression = "java(String.valueOf(entry.getValue()))") - CreateComponentParameter toCreateComponentParameter(Map.Entry entry); + ProvisionActionParameter toCreateComponentParameter(Map.Entry entry); @Named("uuidToString") default String uuidToString(UUID sourceId) { 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 0b67225..3eaedc8 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 @@ -6,6 +6,7 @@ import org.mapstruct.factory.Mappers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; import org.opendevstack.apiservice.project.exception.ComponentCreationException; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; import org.opendevstack.apiservice.project.facade.ComponentsFacade; @@ -50,13 +51,13 @@ void tearDown() throws Exception { } @Test - void create_project_component_returns_ok_when_component_is_created() { + void create_project_component_returns_ok_when_component_is_created() throws MarketplaceException { String projectId = "testProjectId"; CreateComponentRequest request = buildTestCreateComponentRequest(); Component createdComponent = buildTestComponent(); createdComponent.setId("component-123"); - when(componentsFacade.createProjectComponent(eq(projectId), any(CreateComponentRequest.class))) + when(componentsFacade.provisionProjectComponent(eq(projectId), any(CreateComponentRequest.class))) .thenReturn(createdComponent); ResponseEntity response = projectComponentsController.createProjectComponent(projectId, request); @@ -67,39 +68,39 @@ void create_project_component_returns_ok_when_component_is_created() { assertThat(response.getBody().getErrorKey()).isEqualTo("000"); assertThat(response.getBody().getMessage()).isEqualTo("Component created"); assertThat(response.getBody().getPath()).isEqualTo("/api/pub/v0/projects/testProjectId/components/component-123"); - verify(componentsFacade).createProjectComponent(projectId, request); + verify(componentsFacade).provisionProjectComponent(projectId, request); } @Test - void create_project_component_returns_internal_error_when_component_creation_returns_null() { + void create_project_component_returns_internal_error_when_component_creation_returns_null() throws MarketplaceException { String projectId = "testProjectId"; CreateComponentRequest request = buildTestCreateComponentRequest(); - when(componentsFacade.createProjectComponent(eq(projectId), any(CreateComponentRequest.class))) + when(componentsFacade.provisionProjectComponent(eq(projectId), any(CreateComponentRequest.class))) .thenReturn(null); assertThatThrownBy(() -> projectComponentsController.createProjectComponent(projectId, request)) .isInstanceOf(ComponentCreationException.class) .hasMessage("Failed to create component for project 'testProjectId'"); - verify(componentsFacade).createProjectComponent(projectId, request); + verify(componentsFacade).provisionProjectComponent(projectId, request); } @Test - void create_project_component_propagates_exception_when_facade_throws_exception() { + void create_project_component_propagates_exception_when_facade_throws_exception() throws MarketplaceException { String projectId = "testProjectId"; CreateComponentRequest request = buildTestCreateComponentRequest(); - when(componentsFacade.createProjectComponent(eq(projectId), any(CreateComponentRequest.class))) + when(componentsFacade.provisionProjectComponent(eq(projectId), any(CreateComponentRequest.class))) .thenThrow(new RuntimeException("boom")); assertThatThrownBy(() -> projectComponentsController.createProjectComponent(projectId, request)) .isInstanceOf(RuntimeException.class) .hasMessage("boom"); - verify(componentsFacade).createProjectComponent(projectId, request); + verify(componentsFacade).provisionProjectComponent(projectId, request); } @Test - void get_project_component_returns_ok_when_component_exists() { + void get_project_component_returns_ok_when_component_exists() throws MarketplaceException { String projectId = "projectId"; UUID componentId = UUID.randomUUID(); Component testComponent = buildTestComponent(); @@ -114,7 +115,7 @@ void get_project_component_returns_ok_when_component_exists() { } @Test - void get_project_component_throws_not_found_when_component_does_not_exist() { + void get_project_component_throws_not_found_when_component_does_not_exist() throws MarketplaceException { String projectId = "projectId"; UUID componentId = UUID.randomUUID(); 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 feae1fe..8a64805 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 @@ -1,12 +1,15 @@ package org.opendevstack.apiservice.project.facade; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mapstruct.factory.Mappers; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.opendevstack.apiservice.externalservice.marketplace.model.ProjectComponent; +import org.mockito.junit.jupiter.MockitoExtension; +import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; import org.opendevstack.apiservice.project.exception.ComponentCreationException; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; @@ -50,8 +53,8 @@ void tearDown() throws Exception { } @Test - void get_project_component_returns_mapped_component_when_marketplace_returns_data() { - ProjectComponent marketplaceComponent = buildTestMarketplaceComponent(); + void get_project_component_returns_mapped_component_when_marketplace_returns_data() throws MarketplaceException { + ProjectComponentInfo marketplaceComponent = buildTestMarketplaceComponent(); when(marketplaceExternalService.getProjectComponent("testProject", "testComponent")) .thenReturn(marketplaceComponent); @@ -65,7 +68,7 @@ void get_project_component_returns_mapped_component_when_marketplace_returns_dat } @Test - void get_project_component_throws_not_found_when_marketplace_returns_null() { + void get_project_component_throws_not_found_when_marketplace_returns_null() throws MarketplaceException { when(marketplaceExternalService.getProjectComponent("testProject", "testComponent")) .thenReturn(null); @@ -76,31 +79,32 @@ void get_project_component_throws_not_found_when_marketplace_returns_null() { } @Test - void create_project_component_returns_mapped_component_when_marketplace_creates_component() { - ProjectComponent marketplaceComponent = buildTestMarketplaceComponent(); + void create_project_component_returns_mapped_component_when_marketplace_creates_component() throws MarketplaceException { + ProjectComponentInfo marketplaceComponent = buildTestMarketplaceComponent(); CreateComponentRequest request = buildTestCreateComponentRequest(); - when(marketplaceExternalService.createProjectComponent(eq("testProject"), any(List.class))) - .thenReturn(marketplaceComponent); + when(marketplaceExternalService.provisionProjectComponent(eq("testProject"), any(List.class))) + .thenReturn(true); //TODO fix this to return more info - Component createdComponent = componentsFacade.createProjectComponent("testProject", request); + Component createdComponent = componentsFacade.provisionProjectComponent("testProject", request); - assertThat(createdComponent).isNotNull(); - assertThat(createdComponent.getId()).isEqualTo(marketplaceComponent.getComponentId().toString()); - assertThat(createdComponent.getStatus()).isEqualTo(ComponentsStatusDTO.RUNNING); - verify(marketplaceExternalService).createProjectComponent(eq("testProject"), any(List.class)); + //TODO fix this to return more info and assert on it +// assertThat(createdComponent).isNotNull(); +// assertThat(createdComponent.getId()).isEqualTo(marketplaceComponent.getComponentId().toString()); +// assertThat(createdComponent.getStatus()).isEqualTo(ComponentsStatusDTO.RUNNING); + verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), any(List.class)); } @Test - void create_project_component_throws_creation_exception_when_marketplace_returns_null() { + void create_project_component_throws_creation_exception_when_marketplace_returns_null() throws MarketplaceException { CreateComponentRequest request = buildTestCreateComponentRequest(); - when(marketplaceExternalService.createProjectComponent(eq("testProject"), any(List.class))) - .thenReturn(null); + when(marketplaceExternalService.provisionProjectComponent(eq("testProject"), any(List.class))) + .thenReturn(false); //TODO fix this to return more info - assertThatThrownBy(() -> componentsFacade.createProjectComponent("testProject", request)) + assertThatThrownBy(() -> componentsFacade.provisionProjectComponent("testProject", request)) .isInstanceOf(ComponentCreationException.class) .hasMessage("Failed to create component for project 'testProject'"); - verify(marketplaceExternalService).createProjectComponent(eq("testProject"), any(List.class)); + verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), any(List.class)); } } \ No newline at end of file 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 0b81f7b..1abb7b2 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,6 +1,7 @@ package org.opendevstack.apiservice.project.util; import org.opendevstack.apiservice.externalservice.marketplace.model.ProjectComponent; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.ComponentsStatusDTO; import org.opendevstack.apiservice.project.model.CreateComponentRequest; @@ -25,9 +26,9 @@ public static Component buildTestComponent() { return component; } - public static ProjectComponent buildTestMarketplaceComponent() { - ProjectComponent component = new ProjectComponent(); - component.setComponentId(UUID.randomUUID()); + public static ProjectComponentInfo buildTestMarketplaceComponent() { + ProjectComponentInfo component = new ProjectComponentInfo(); + component.setComponentId(UUID.randomUUID().toString()); component.setStatus("RUNNING"); component.setCanBeDeleted(false); component.setComponentUrl("http://test.component.url"); 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 7ab1151..e68ee9b 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 @@ -101,6 +101,8 @@ void testGetProjectComponent_RestClientException() throws MarketplaceException { when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); + when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenThrow(new RestClientException("Connection failed")); // Act & Assert assertThrows(MarketplaceException.class, () -> From 4138b18d6b1cedfa3ff899d018f59829e1e63bc0 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Wed, 22 Apr 2026 14:41:29 +0200 Subject: [PATCH 07/25] Implement component existence check and exception handling for duplicate components and added mandatory parameters to the marketplace 2.0 provision component call --- .../openapi/api-project-component-v0.yaml | 1 - .../controller/ComponentsResponseFactory.java | 4 ++ .../ProjectComponentsController.java | 27 +++----- .../ProjectComponentsExceptionHandler.java | 17 +++++ .../ComponentAlreadyExistsException.java | 12 ++++ .../project/facade/ComponentsFacade.java | 36 ++++++++--- .../project/mapper/MarketplaceMapper.java | 54 +++++----------- .../ProjectComponentsControllerTest.java | 47 +++++--------- ...ProjectComponentsExceptionHandlerTest.java | 15 +++++ .../project/facade/ComponentsFacadeTest.java | 41 +++++++++--- .../client/MarketplaceApiClient.java | 2 +- .../client/MarketplaceApiClientFactory.java | 2 +- .../config/MarketplaceInstanceConfig.java | 62 +++++++++++++++++++ .../config/MarketplaceServiceConfig.java | 55 ---------------- .../service/impl/MarketplaceServiceImpl.java | 22 ++++++- .../MarketplaceApiClientFactoryTest.java | 7 ++- .../service/MarketplaceServiceImplTest.java | 30 ++++++++- 17 files changed, 263 insertions(+), 171 deletions(-) create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentAlreadyExistsException.java create mode 100644 external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceInstanceConfig.java diff --git a/api-project-component-v0/openapi/api-project-component-v0.yaml b/api-project-component-v0/openapi/api-project-component-v0.yaml index c85d72a..4e331b4 100644 --- a/api-project-component-v0/openapi/api-project-component-v0.yaml +++ b/api-project-component-v0/openapi/api-project-component-v0.yaml @@ -90,7 +90,6 @@ paths: description: Component id schema: type: string - format: uuid responses: '200': description: Component information diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java index 28fa4a0..9e6cb70 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java @@ -22,6 +22,10 @@ public static CreateComponentResponse forbidden(String path, String message, Com return buildResponse(HttpStatus.FORBIDDEN, errorKey, path, message); } + public static CreateComponentResponse conflict(String path, String message, ComponentErrorKey errorKey) { + return buildResponse(HttpStatus.CONFLICT, errorKey, path, message); + } + public static CreateComponentResponse notFound(String path, String message, ComponentErrorKey errorKey) { return buildResponse(HttpStatus.NOT_FOUND, errorKey, path, message); } 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 e48c6f7..e168e06 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 @@ -4,7 +4,6 @@ import lombok.extern.slf4j.Slf4j; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; import org.opendevstack.apiservice.project.api.ProjectComponentsApi; -import org.opendevstack.apiservice.project.exception.ComponentCreationException; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; import org.opendevstack.apiservice.project.facade.ComponentsFacade; import org.opendevstack.apiservice.project.mapper.ComponentResponseMapper; @@ -16,8 +15,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.UUID; - @RestController @AllArgsConstructor @Slf4j @@ -32,30 +29,20 @@ public class ProjectComponentsController implements ProjectComponentsApi { @Override public ResponseEntity createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { - try { - Component component = componentsFacade.provisionProjectComponent(projectId, createComponentRequest); - if (component == null) { - throw new ComponentCreationException(String.format("Failed to create component for project '%s'", projectId)); - } + componentsFacade.provisionProjectComponent(projectId, createComponentRequest); - log.info("Created component {} for project id {} and request {}", component, projectId, createComponentRequest); - return componentResponseMapper.toResponseEntity( - ComponentsResponseFactory.entityCreated(projectId, component.getId()) - ); - } catch (MarketplaceException e) { //TODO use error handler - log.error("Error while creating component for project '{}': {}", projectId, e.getMessage(), e); - throw new ComponentCreationException( - String.format("Failed to create component for project '%s': %s", projectId, e.getMessage()), e - ); - } + log.info("Created component '{}' for project '{}'", createComponentRequest.getName(), projectId); + return componentResponseMapper.toResponseEntity( + ComponentsResponseFactory.entityCreated(projectId, createComponentRequest.getName()) + ); } @Override - public ResponseEntity getProjectComponent(String projectId, UUID componentId) { + public ResponseEntity getProjectComponent(String projectId, String componentId) { try { - Component component = componentsFacade.getProjectComponent(projectId, componentId.toString()); + Component component = componentsFacade.getProjectComponent(projectId, componentId); if (component == null) { throw new ComponentNotFoundException( String.format("Component '%s' not found for project '%s'", componentId, projectId) 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 f561d3d..f83ac96 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 @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.opendevstack.apiservice.project.controller.ComponentsResponseFactory; import org.opendevstack.apiservice.project.controller.ProjectComponentsController; +import org.opendevstack.apiservice.project.exception.ComponentAlreadyExistsException; import org.opendevstack.apiservice.project.exception.ComponentCreationException; import org.opendevstack.apiservice.project.exception.ComponentErrorKey; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; @@ -138,6 +139,22 @@ public ResponseEntity handleComponentCreationException( return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); } + @ExceptionHandler(ComponentAlreadyExistsException.class) + public ResponseEntity handleComponentAlreadyExistsException( + ComponentAlreadyExistsException ex, + HttpServletRequest request) { + + log.warn("Component already exists: {}", ex.getMessage()); + + CreateComponentResponse response = ComponentsResponseFactory.conflict( + request.getRequestURI(), + ex.getMessage(), + ComponentErrorKey.INVALID_PARAMETERS + ); + + return ResponseEntity.status(HttpStatus.CONFLICT).body(response); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleGenericException( Exception ex, diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentAlreadyExistsException.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentAlreadyExistsException.java new file mode 100644 index 0000000..8435586 --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentAlreadyExistsException.java @@ -0,0 +1,12 @@ +package org.opendevstack.apiservice.project.exception; + +public class ComponentAlreadyExistsException extends RuntimeException { + + public ComponentAlreadyExistsException(String message) { + super(message); + } + + public ComponentAlreadyExistsException(String message, Exception e) { + super(message, e); + } +} \ No newline at end of file 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 96849e0..f590b55 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 @@ -6,12 +6,14 @@ import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; 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.ComponentNotFoundException; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.CreateComponentRequest; import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; import java.util.List; @@ -35,17 +37,35 @@ public Component getProjectComponent(String projectId, String componentId) throw return marketplaceMapper.mapMarketplaceComponentToV0Component(marketplaceComponent); } - public Component provisionProjectComponent(String projectId, CreateComponentRequest createComponentRequest) throws MarketplaceException { - List createComponentParameterList = marketplaceMapper.mapCreateComponentRequestToCreateComponentParameterList(createComponentRequest); - //TODO implement this with more info from marketplace - boolean success = marketplaceExternalService.provisionProjectComponent(projectId, createComponentParameterList); - if (!success) { - log.error("Failed to create component in marketplace for project with id {}", projectId); + public void provisionProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { + try { + List createComponentParameterList = marketplaceMapper.mapCreateComponentRequestToCreateComponentParameterList(createComponentRequest); + boolean success = marketplaceExternalService.provisionProjectComponent(projectId, createComponentParameterList); + if (!success) { + log.error("Failed to create component in marketplace for project with id {}", projectId); + throw new ComponentCreationException( + String.format("Failed to create component for project '%s'", projectId) + ); + } + } catch (MarketplaceException e) { + if (isConflictCause(e)) { + throw new ComponentAlreadyExistsException(e.getMessage(), e); + } throw new ComponentCreationException( - String.format("Failed to create component for project '%s'", projectId) + String.format("Failed to create component for project '%s': %s", projectId, e.getMessage()), e ); } - return marketplaceMapper.mapMarketplaceComponentToV0Component(new ProjectComponentInfo()); //TODO get the created component info from marketplace and map it + } + + private boolean isConflictCause(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof HttpClientErrorException.Conflict) { + return true; + } + current = current.getCause(); + } + return false; } public Boolean deleteProjectComponent(String projectId, String componentId) { 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 5119396..a81d099 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 @@ -7,30 +7,37 @@ import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter; import org.opendevstack.apiservice.project.model.Component; -import org.opendevstack.apiservice.project.model.ComponentsStatusDTO; import org.opendevstack.apiservice.project.model.CreateComponentRequest; -import org.opendevstack.apiservice.project.model.EnvironmentsDTO; +import java.util.ArrayList; import java.util.List; import java.util.Map; -import java.util.UUID; @Mapper(componentModel = "spring") public interface MarketplaceMapper { - @Mapping(target = "id", source = "componentId", qualifiedByName = "uuidToString") -// TODO @Mapping(target = "environment", source = "environment", qualifiedByName = "toEnvironment") - @Mapping(target = "status", source = "status", qualifiedByName = "toComponentStatus") -// TODO @Mapping(target = "params", expression = "java(java.util.Collections.emptyMap())") + @Mapping(target = "id", source = "componentId") @Mapping(target = "resultTraceback", ignore = true) Component mapMarketplaceComponentToV0Component(ProjectComponentInfo source); default List mapCreateComponentRequestToCreateComponentParameterList(CreateComponentRequest createComponentRequest) { - if (createComponentRequest == null || createComponentRequest.getParams() == null) { + if (createComponentRequest == null) { return List.of(); } - return mapEntriesToCreateComponentParameterList(createComponentRequest.getParams().entrySet().stream().toList()); + List parameters = new ArrayList<>(); + parameters.add(createParameter("component_id", createComponentRequest.getName(), "string")); + parameters.add(createParameter("component_type", createComponentRequest.getProductId(), "string")); + + if (createComponentRequest.getParams() != null && !createComponentRequest.getParams().isEmpty()) { + parameters.addAll(mapEntriesToCreateComponentParameterList(createComponentRequest.getParams().entrySet().stream().toList())); + } + + return parameters; + } + + default ProvisionActionParameter createParameter(String name, String value, String type) { + return new ProvisionActionParameter().name(name).type(type).value(value); } @IterableMapping(qualifiedByName = "toCreateComponentParameter") @@ -41,33 +48,4 @@ default List mapCreateComponentRequestToCreateComponen @Mapping(target = "type", constant = "string") @Mapping(target = "value", expression = "java(String.valueOf(entry.getValue()))") ProvisionActionParameter toCreateComponentParameter(Map.Entry entry); - - @Named("uuidToString") - default String uuidToString(UUID sourceId) { - return sourceId != null ? sourceId.toString() : null; - } - - @Named("toComponentStatus") - default ComponentsStatusDTO toComponentStatus(String sourceStatus) { - if (sourceStatus == null || sourceStatus.isBlank()) { - return null; - } - try { - return ComponentsStatusDTO.fromValue(sourceStatus); - } catch (IllegalArgumentException ex) { - return null; - } - } - - @Named("toEnvironment") - default EnvironmentsDTO toEnvironment(String sourceEnv) { - if (sourceEnv == null || sourceEnv.isBlank()) { - return null; - } - try { - return EnvironmentsDTO.fromValue(sourceEnv); - } catch (IllegalArgumentException ex) { - return null; - } - } } 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 3eaedc8..8142c12 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 @@ -7,7 +7,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; -import org.opendevstack.apiservice.project.exception.ComponentCreationException; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; import org.opendevstack.apiservice.project.facade.ComponentsFacade; import org.opendevstack.apiservice.project.mapper.ComponentResponseMapper; @@ -17,12 +16,11 @@ import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import java.util.UUID; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +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.buildTestComponent; @@ -51,14 +49,11 @@ void tearDown() throws Exception { } @Test - void create_project_component_returns_ok_when_component_is_created() throws MarketplaceException { + void create_project_component_returns_ok_with_component_name_in_path() { String projectId = "testProjectId"; - CreateComponentRequest request = buildTestCreateComponentRequest(); - Component createdComponent = buildTestComponent(); - createdComponent.setId("component-123"); + CreateComponentRequest request = buildTestCreateComponentRequest(); // name = "testcomponent" - when(componentsFacade.provisionProjectComponent(eq(projectId), any(CreateComponentRequest.class))) - .thenReturn(createdComponent); + doNothing().when(componentsFacade).provisionProjectComponent(eq(projectId), any(CreateComponentRequest.class)); ResponseEntity response = projectComponentsController.createProjectComponent(projectId, request); @@ -67,31 +62,17 @@ void create_project_component_returns_ok_when_component_is_created() throws Mark 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/component-123"); - verify(componentsFacade).provisionProjectComponent(projectId, request); - } - - @Test - void create_project_component_returns_internal_error_when_component_creation_returns_null() throws MarketplaceException { - String projectId = "testProjectId"; - CreateComponentRequest request = buildTestCreateComponentRequest(); - - when(componentsFacade.provisionProjectComponent(eq(projectId), any(CreateComponentRequest.class))) - .thenReturn(null); - - assertThatThrownBy(() -> projectComponentsController.createProjectComponent(projectId, request)) - .isInstanceOf(ComponentCreationException.class) - .hasMessage("Failed to create component for project 'testProjectId'"); + assertThat(response.getBody().getPath()).isEqualTo("/api/pub/v0/projects/testProjectId/components/testcomponent"); verify(componentsFacade).provisionProjectComponent(projectId, request); } @Test - void create_project_component_propagates_exception_when_facade_throws_exception() throws MarketplaceException { + void create_project_component_propagates_exception_when_facade_throws_exception() { String projectId = "testProjectId"; CreateComponentRequest request = buildTestCreateComponentRequest(); - when(componentsFacade.provisionProjectComponent(eq(projectId), any(CreateComponentRequest.class))) - .thenThrow(new RuntimeException("boom")); + org.mockito.Mockito.doThrow(new RuntimeException("boom")) + .when(componentsFacade).provisionProjectComponent(eq(projectId), any(CreateComponentRequest.class)); assertThatThrownBy(() -> projectComponentsController.createProjectComponent(projectId, request)) .isInstanceOf(RuntimeException.class) @@ -102,28 +83,28 @@ void create_project_component_propagates_exception_when_facade_throws_exception( @Test void get_project_component_returns_ok_when_component_exists() throws MarketplaceException { String projectId = "projectId"; - UUID componentId = UUID.randomUUID(); + String componentId = "test-component-one"; Component testComponent = buildTestComponent(); - when(componentsFacade.getProjectComponent(projectId, componentId.toString())).thenReturn(testComponent); + when(componentsFacade.getProjectComponent(projectId, componentId)).thenReturn(testComponent); ResponseEntity response = projectComponentsController.getProjectComponent(projectId, componentId); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isEqualTo(testComponent); - verify(componentsFacade).getProjectComponent(projectId, componentId.toString()); + verify(componentsFacade).getProjectComponent(projectId, componentId); } @Test void get_project_component_throws_not_found_when_component_does_not_exist() throws MarketplaceException { String projectId = "projectId"; - UUID componentId = UUID.randomUUID(); + String componentId = "test-component-one"; - when(componentsFacade.getProjectComponent(projectId, componentId.toString())).thenReturn(null); + when(componentsFacade.getProjectComponent(projectId, componentId)).thenReturn(null); assertThatThrownBy(() -> projectComponentsController.getProjectComponent(projectId, componentId)) .isInstanceOf(ComponentNotFoundException.class) .hasMessage("Component '" + componentId + "' not found for project 'projectId'"); - verify(componentsFacade).getProjectComponent(projectId, componentId.toString()); + 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/advice/ProjectComponentsExceptionHandlerTest.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandlerTest.java index e6bced2..92a0601 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 @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +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.model.CreateComponentResponse; @@ -134,6 +135,20 @@ void handle_component_creation_exception_returns_internal_server_error() { assertThat(response.getBody().getMessage()).isEqualTo("Creation failed"); } + @Test + void handle_component_already_exists_exception_returns_conflict() { + ComponentAlreadyExistsException exception = new ComponentAlreadyExistsException( + "This component name already exists, please choose another name."); + + ResponseEntity response = handler.handleComponentAlreadyExistsException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getHttpStatus()).isEqualTo(HttpStatus.CONFLICT.name()); + assertThat(response.getBody().getErrorKey()).isEqualTo("006"); + assertThat(response.getBody().getMessage()).isEqualTo("This component name already exists, please choose another name."); + } + @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 8a64805..fc9a465 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 @@ -11,18 +11,20 @@ import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; 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.ComponentNotFoundException; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.ComponentsStatusDTO; import org.opendevstack.apiservice.project.model.CreateComponentRequest; - -import java.util.List; +import org.springframework.http.HttpHeaders; +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.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -62,7 +64,7 @@ void get_project_component_returns_mapped_component_when_marketplace_returns_dat Component retrievedComponent = componentsFacade.getProjectComponent("testProject", "testComponent"); assertThat(retrievedComponent).isNotNull(); - assertThat(retrievedComponent.getId()).isEqualTo(marketplaceComponent.getComponentId().toString()); + assertThat(retrievedComponent.getId()).isNotNull(); assertThat(retrievedComponent.getStatus()).isEqualTo(ComponentsStatusDTO.RUNNING); verify(marketplaceExternalService).getProjectComponent("testProject", "testComponent"); } @@ -80,31 +82,50 @@ void get_project_component_throws_not_found_when_marketplace_returns_null() thro @Test void create_project_component_returns_mapped_component_when_marketplace_creates_component() throws MarketplaceException { - ProjectComponentInfo marketplaceComponent = buildTestMarketplaceComponent(); CreateComponentRequest request = buildTestCreateComponentRequest(); - when(marketplaceExternalService.provisionProjectComponent(eq("testProject"), any(List.class))) + when(marketplaceExternalService.provisionProjectComponent(eq("testProject"), anyList())) .thenReturn(true); //TODO fix this to return more info - Component createdComponent = componentsFacade.provisionProjectComponent("testProject", request); + componentsFacade.provisionProjectComponent("testProject", request); //TODO fix this to return more info and assert on it // assertThat(createdComponent).isNotNull(); // assertThat(createdComponent.getId()).isEqualTo(marketplaceComponent.getComponentId().toString()); // assertThat(createdComponent.getStatus()).isEqualTo(ComponentsStatusDTO.RUNNING); - verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), any(List.class)); + verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), anyList()); } @Test void create_project_component_throws_creation_exception_when_marketplace_returns_null() throws MarketplaceException { CreateComponentRequest request = buildTestCreateComponentRequest(); - when(marketplaceExternalService.provisionProjectComponent(eq("testProject"), any(List.class))) + when(marketplaceExternalService.provisionProjectComponent(eq("testProject"), anyList())) .thenReturn(false); //TODO fix this to return more info assertThatThrownBy(() -> componentsFacade.provisionProjectComponent("testProject", request)) .isInstanceOf(ComponentCreationException.class) .hasMessage("Failed to create component for project 'testProject'"); - verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), any(List.class)); + verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), anyList()); } + + @Test + void create_project_component_throws_already_exists_when_marketplace_returns_conflict() throws MarketplaceException { + CreateComponentRequest request = buildTestCreateComponentRequest(); + HttpClientErrorException conflict = HttpClientErrorException.Conflict.create( + HttpStatus.CONFLICT, + "Conflict", + HttpHeaders.EMPTY, + new byte[0], + null + ); + + when(marketplaceExternalService.provisionProjectComponent(eq("testProject"), anyList())) + .thenThrow(new MarketplaceException("This component name already exists, please choose another name.", conflict)); + + assertThatThrownBy(() -> componentsFacade.provisionProjectComponent("testProject", request)) + .isInstanceOf(ComponentAlreadyExistsException.class) + .hasMessage("This component name already exists, please choose another name."); + verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), anyList()); + } } \ No newline at end of file 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 995ec5a..9fd4642 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 @@ -4,7 +4,7 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; import org.openapitools.jackson.nullable.JsonNullableModule; -import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig.MarketplaceInstanceConfig; +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.springframework.http.converter.HttpMessageConverter; 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 92ac54e..70cc2d0 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 @@ -1,8 +1,8 @@ package org.opendevstack.apiservice.externalservice.marketplace.client; import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceInstanceConfig; import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig; -import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig.MarketplaceInstanceConfig; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.cache.annotation.CacheEvict; 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 new file mode 100644 index 0000000..0414f20 --- /dev/null +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceInstanceConfig.java @@ -0,0 +1,62 @@ +package org.opendevstack.apiservice.externalservice.marketplace.config; + +import lombok.Data; + +@Data +public class MarketplaceInstanceConfig { + /** + * The project components base URL of the Marketplace + */ + private String projectComponentsBaseUrl; + + /** + * The provisioner actions base URL of the Marketplace + */ + private String provisionerActionsBaseUrl; + + /** + * Authentication access token for accessing the Marketplace API + */ + private String accessToken; + + /** + * Authentication bearer token for accessing the Marketplace API + */ + private String bearerToken; + + /** + * Username for authentication (used with password for basic auth). + * Only used if bearerToken is not provided. + */ + private String username; + + /** + * Password or personal access token for authentication. + * Only used if bearerToken is not provided. + */ + private String password; + + /** + * Connection timeout in milliseconds (default: 30000) + */ + private int connectionTimeout = 30000; + + /** + * Read timeout in milliseconds (default: 30000). + */ + private int readTimeout = 30000; + + /** + * Whether to trust all SSL certificates (default: false). + * WARNING: Should only be used in development environments. + */ + private boolean trustAllCertificates = false; + + private String workflow; + + private String odsNamespace; + + private String quickstarterRepository; + + private String catalogItemId; +} \ No newline at end of file diff --git a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceServiceConfig.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceServiceConfig.java index 2a0f593..bbaec73 100644 --- a/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceServiceConfig.java +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/config/MarketplaceServiceConfig.java @@ -22,59 +22,4 @@ public class MarketplaceServiceConfig { * Map of Marketplace instances with the instance name as the key and the configuration as the value. */ private Map instances = new HashMap<>(); - - /** - * Configuration for a single Jira instance. - */ - @Data - public static class MarketplaceInstanceConfig { - - /** - * The project components base URL of the Marketplace - */ - private String projectComponentsBaseUrl; - - /** - * The provisioner actions base URL of the Marketplace - */ - private String provisionerActionsBaseUrl; - - /** - * Authentication access token for accessing the Marketplace API - */ - private String accessToken; - - /** - * Authentication bearer token for accessing the Marketplace API - */ - private String bearerToken; - - /** - * Username for authentication (used with password for basic auth). - * Only used if bearerToken is not provided. - */ - private String username; - - /** - * Password or personal access token for authentication. - * Only used if bearerToken is not provided. - */ - private String password; - - /** - * Connection timeout in milliseconds (default: 30000) - */ - private int connectionTimeout = 30000; - - /** - * Read timeout in milliseconds (default: 30000). - */ - private int readTimeout = 30000; - - /** - * Whether to trust all SSL certificates (default: false). - * WARNING: Should only be used in development environments. - */ - private boolean trustAllCertificates = false; - } } 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 ac9b526..67767d6 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 @@ -3,6 +3,7 @@ import lombok.extern.slf4j.Slf4j; import org.opendevstack.apiservice.externalservice.marketplace.client.MarketplaceApiClient; 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.ProjectComponentsApi; @@ -80,17 +81,36 @@ public boolean provisionProjectComponent(String instanceName, String projectId, try { MarketplaceApiClient marketplaceClient = clientFactory.getClient(instanceName); ApiClient apiClient = marketplaceClient.getApiClient(); + MarketplaceInstanceConfig config = marketplaceClient.getConfig(); + + String provisionerActionsBaseUrl = config.getProvisionerActionsBaseUrl(); + String accessToken = config.getAccessToken(); + String workflow = config.getWorkflow(); + String odsNamespace = config.getOdsNamespace(); + String quickstarterRepository = config.getQuickstarterRepository(); + String catalogItemId = config.getCatalogItemId(); ProvisionAction provisionAction = new ProvisionAction(); provisionAction.setId("PROVISION"); provisionAction.addParametersItem(new ProvisionActionParameter().name("project_key").type("string").value(projectId)); + + //TODO: currently in dev is not working but, we need to add + //TODO: the error 500 on POST request for "https://component-provisioner-devstack-dev.apps.eu-dev.ocp.aws.boehringer.com/v1/provision-actions": "{"message":"Duplicate key access_token (attempted merging values [...]])"}" +// provisionAction.addParametersItem(new ProvisionActionParameter().name("access_token").type("string").value(accessToken)); + provisionAction.addParametersItem(new ProvisionActionParameter().name("workflow").type("string").value(workflow)); + provisionAction.addParametersItem(new ProvisionActionParameter().name("ods_namespace").type("string").value(odsNamespace)); + provisionAction.addParametersItem(new ProvisionActionParameter().name("quickstarter_repo").type("string").value(quickstarterRepository)); + provisionAction.addParametersItem(new ProvisionActionParameter().name("catalog_item_id").type("string").value(catalogItemId)); + params.forEach(provisionAction::addParametersItem); ProvisionerActionsApi provisionerActionsApi = new ProvisionerActionsApi(apiClient); - apiClient.setBasePath(marketplaceClient.getConfig().getProvisionerActionsBaseUrl()); + apiClient.setBasePath(provisionerActionsBaseUrl); ProvisionActionResponse response = provisionerActionsApi.triggerProvisionAction(provisionAction); return !response.getFailed(); + } catch (HttpClientErrorException.Conflict e) { + throw new MarketplaceException("This component name already exists, please choose another name.", e); } catch (HttpClientErrorException.Unauthorized | HttpClientErrorException.Forbidden e) { throw new MarketplaceException( String.format("Access denied when provisioning project component in project '%s' and instance '%s'", diff --git a/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClientFactoryTest.java b/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClientFactoryTest.java index ecb32a5..d8bdbfe 100644 --- a/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClientFactoryTest.java +++ b/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/client/MarketplaceApiClientFactoryTest.java @@ -5,8 +5,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceInstanceConfig; import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig; -import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig.MarketplaceInstanceConfig; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.web.client.RestTemplate; @@ -14,7 +14,10 @@ import java.util.LinkedHashMap; import java.util.Map; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.when; 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 e68ee9b..78b73fd 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 @@ -7,7 +7,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.opendevstack.apiservice.externalservice.marketplace.client.MarketplaceApiClient; import org.opendevstack.apiservice.externalservice.marketplace.client.MarketplaceApiClientFactory; -import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig.MarketplaceInstanceConfig; +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.ProjectComponentInfo; @@ -20,6 +20,7 @@ import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestClientException; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.List; import java.util.Set; @@ -337,4 +338,31 @@ void testGetDefaultInstance_FactoryThrows_PropagatesException() throws Marketpla assertThrows(MarketplaceException.class, () -> marketplaceService.getDefaultInstance()); } + @Test + void testProvisionProjectComponent_Conflict_ThrowsMarketplaceExceptionWithDuplicateMessage() throws MarketplaceException { + String instanceName = "dev"; + String projectKey = "EDPC"; + MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); + instanceConfig.setProvisionerActionsBaseUrl("https://example/provision-actions"); + + 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 + ); + + when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); + when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); + when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); + when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenThrow(conflictEx); + + MarketplaceException exception = assertThrows(MarketplaceException.class, () -> + marketplaceService.provisionProjectComponent(instanceName, projectKey, List.of())); + + assertEquals("This component name already exists, please choose another name.", exception.getMessage()); + } + } \ No newline at end of file From 911780e2ae91f67f78e15d5aa8257936478bfba1 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Mon, 27 Apr 2026 15:28:55 +0200 Subject: [PATCH 08/25] Add API endpoint to retrieve extended information of a project component by project key and component ID --- .../openapi-component_catalog-v1.0.0.yaml | 77 +++++++++++++++++++ external-service-marketplace/pom.xml | 2 +- 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/external-service-marketplace/openapi/openapi-component_catalog-v1.0.0.yaml b/external-service-marketplace/openapi/openapi-component_catalog-v1.0.0.yaml index 82ca393..7c43715 100644 --- a/external-service-marketplace/openapi/openapi-component_catalog-v1.0.0.yaml +++ b/external-service-marketplace/openapi/openapi-component_catalog-v1.0.0.yaml @@ -75,6 +75,50 @@ paths: application/json: schema: $ref: '#/components/schemas/RestErrorMessage' + /project/{projectKey}/component/{componentId}: + get: + tags: + - Project-components + summary: Returns the extended information of a project component given both its project key and component ID in the Bitbucket repository. + operationId: getProjectComponentById + parameters: + - name: projectKey + in: path + description: project key. + required: true + schema: + type: string + - name: componentId + in: path + description: component ID. + required: true + schema: + type: string + responses: + "200": + description: The extended information of a project component. + content: + application/json: + schema: + $ref: '#/components/schemas/ProjectComponentExtendedInfo' + "401": + description: Invalid client token on the request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "403": + description: Insufficient permissions for the client to access the resource. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "500": + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' /catalog-descriptors: get: tags: @@ -853,6 +897,39 @@ components: componentUrl: type: string example: 'https://bitbucket.com/projects/CATALOGS/repos/project-components/browse/projects' + ProjectComponentParameter: + properties: + name: + type: string + example: 'environment' + values: + type: array + items: + type: string + example: + - 'dev' + - 'test' + ProjectComponentExtendedInfo: + properties: + 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' + parameters: + type: array + items: + $ref: '#/components/schemas/ProjectComponentParameter' CatalogDescriptor: properties: id: diff --git a/external-service-marketplace/pom.xml b/external-service-marketplace/pom.xml index d5134ea..a37e94b 100644 --- a/external-service-marketplace/pom.xml +++ b/external-service-marketplace/pom.xml @@ -150,7 +150,7 @@ generate - FILTER=operationId:getProjectComponents + FILTER=operationId:getProjectComponents|getProjectComponentById java ${project.basedir} resttemplate From 3d708a6d0b136bfd45e5db2379bfda70de0ee870 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Wed, 29 Apr 2026 14:20:33 +0200 Subject: [PATCH 09/25] Update getComponent to call the new endpoint. --- .../ProjectComponentsController.java | 20 ++----- .../ProjectComponentsExceptionHandler.java | 12 ++++ .../CatalogItemNotFoundException.java | 12 ++++ .../ComponentRetrievalException.java | 12 ++++ .../project/facade/ComponentsFacade.java | 37 +++++++++--- .../project/mapper/MarketplaceMapper.java | 28 +++++++-- .../apiservice/project/mapper/StatusMap.java | 23 ++++++++ .../project/facade/ComponentsFacadeTest.java | 10 +++- .../project/util/TestObjectsBuilder.java | 21 +++++-- external-service-marketplace/pom.xml | 2 +- .../service/CatalogItemOperations.java | 25 ++++++++ .../service/MarketplaceService.java | 11 +++- .../service/impl/MarketplaceServiceImpl.java | 47 +++++++++++---- .../service/CatalogItemOperationsTest.java | 39 +++++++++++++ .../service/MarketplaceServiceImplTest.java | 58 +++++++++++++++++-- 15 files changed, 303 insertions(+), 54 deletions(-) create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/CatalogItemNotFoundException.java create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentRetrievalException.java create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/StatusMap.java create mode 100644 external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/CatalogItemOperations.java create mode 100644 external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/service/CatalogItemOperationsTest.java 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 e168e06..af47cca 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 @@ -40,23 +40,15 @@ public ResponseEntity createProjectComponent(String pro @Override public ResponseEntity getProjectComponent(String projectId, String componentId) { - try { - - Component component = componentsFacade.getProjectComponent(projectId, componentId); - if (component == null) { - throw new ComponentNotFoundException( - String.format("Component '%s' not found for project '%s'", componentId, projectId) - ); - } - - log.info("Retrieved component '{}' for project '{}': {}", componentId, projectId, component); - return ResponseEntity.status(HttpStatus.OK).body(component); - } catch (MarketplaceException e) { //TODO use error handler - log.error("Error while retrieving component '{}' for project '{}': {}", componentId, projectId, e.getMessage(), e); + Component component = componentsFacade.getProjectComponent(projectId, componentId); + if (component == null) { throw new ComponentNotFoundException( - String.format("Failed to retrieve component '%s' for project '%s': %s", componentId, projectId, e.getMessage()), e + String.format("Component '%s' not found for project '%s'", componentId, projectId) ); } + + log.info("Retrieved component '{}' for project '{}': {}", componentId, projectId, component); + return ResponseEntity.status(HttpStatus.OK).body(component); } } 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 f83ac96..160e411 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 @@ -8,6 +8,8 @@ import org.opendevstack.apiservice.project.exception.ComponentCreationException; 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; @@ -139,6 +141,16 @@ public ResponseEntity handleComponentCreationException( return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); } + @ExceptionHandler(ComponentRetrievalException.class) + public ResponseEntity handleComponentRetrievalException( + ComponentRetrievalException ex, + HttpServletRequest request) { + + log.error("Component retrieval failed: {}", ex.getMessage(), ex); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); + } + @ExceptionHandler(ComponentAlreadyExistsException.class) public ResponseEntity handleComponentAlreadyExistsException( ComponentAlreadyExistsException ex, diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/CatalogItemNotFoundException.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/CatalogItemNotFoundException.java new file mode 100644 index 0000000..9b66377 --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/CatalogItemNotFoundException.java @@ -0,0 +1,12 @@ +package org.opendevstack.apiservice.project.exception; + +public class CatalogItemNotFoundException extends RuntimeException { + + public CatalogItemNotFoundException(String message) { + super(message); + } + + public CatalogItemNotFoundException(String message, Exception e) { + super(message, e); + } +} diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentRetrievalException.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentRetrievalException.java new file mode 100644 index 0000000..655c5be --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentRetrievalException.java @@ -0,0 +1,12 @@ +package org.opendevstack.apiservice.project.exception; + +public class ComponentRetrievalException extends RuntimeException { + + public ComponentRetrievalException(String message) { + super(message); + } + + public ComponentRetrievalException(String message, Exception e) { + super(message, e); + } +} \ No newline at end of file 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 f590b55..685a91f 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,12 +3,16 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; +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.service.CatalogItemOperations; import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; +import org.opendevstack.apiservice.project.exception.CatalogItemNotFoundException; 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.ComponentRetrievalException; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.CreateComponentRequest; @@ -26,15 +30,32 @@ public class ComponentsFacade { private final MarketplaceMapper marketplaceMapper; - public Component getProjectComponent(String projectId, String componentId) throws MarketplaceException { - ProjectComponentInfo marketplaceComponent = marketplaceExternalService.getProjectComponent(projectId, componentId); - if (marketplaceComponent == null) { - log.info("Marketplace component with id {} not found", componentId); - throw new ComponentNotFoundException( - String.format("Component '%s' not found for project '%s'", componentId, projectId) + public Component getProjectComponent(String projectId, String componentId) { + try { + ProjectComponentExtendedInfo marketplaceComponent = marketplaceExternalService.getProjectComponent(projectId, componentId); + if (marketplaceComponent == null) { + log.info("Marketplace component with id {} not found", componentId); + throw new ComponentNotFoundException( + String.format("Component '%s' not found for project '%s'", componentId, projectId) + ); + } + String catalogItemId = CatalogItemOperations.buildCatalogItemId(marketplaceComponent); + CatalogItem catalogItem = marketplaceExternalService.getCatalogItem(catalogItemId); + if (catalogItem == null) { + log.info("Catalog item with id {} not found", catalogItemId); + throw new CatalogItemNotFoundException( + String.format("Catalog item with id '%s' not found", catalogItemId) + ); + } + Component component = marketplaceMapper.mapMarketplaceComponentToV0Component(marketplaceComponent, catalogItem); + log.info("Marketplace v0 component retrieved: {}", component); + return component; + } catch (MarketplaceException e) { + log.error("Failed to retrieve component with id {} for project with id {}: {}", componentId, projectId, e.getMessage(), e); + throw new ComponentRetrievalException( + String.format("Failed to retrieve component '%s' for project '%s': %s", componentId, projectId, e.getMessage()), e ); } - return marketplaceMapper.mapMarketplaceComponentToV0Component(marketplaceComponent); } public void provisionProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { 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 a81d099..c307b60 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 @@ -1,13 +1,17 @@ package org.opendevstack.apiservice.project.mapper; +import lombok.extern.slf4j.Slf4j; import org.mapstruct.IterableMapping; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Named; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; +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.project.model.Component; import org.opendevstack.apiservice.project.model.CreateComponentRequest; +import org.opendevstack.apiservice.project.model.EnvironmentsDTO; import java.util.ArrayList; import java.util.List; @@ -16,9 +20,25 @@ @Mapper(componentModel = "spring") public interface MarketplaceMapper { - @Mapping(target = "id", source = "componentId") - @Mapping(target = "resultTraceback", ignore = true) - Component mapMarketplaceComponentToV0Component(ProjectComponentInfo source); + default Component mapMarketplaceComponentToV0Component(ProjectComponentExtendedInfo source, CatalogItem catalogItem) throws MarketplaceException { + Component component = new Component(); + component.setId(source.getComponentId()); + component.setEnvironment(EnvironmentsDTO.DEV); // Env is always DEV so we hardcode it as such + component.setRepositoryURL(source.getComponentUrl()); + component.setComponentType(""); // We agreed to hardcode the type as empty + component.setStatus(StatusMap.toOldStatus(source.getStatus())); + if (component.getStatus() == null) { + throw new MarketplaceException("No status mapping found for status " + source.getStatus()); + } + component.setProductId(catalogItem.getId()); + component.setProductName(catalogItem.getTitle()); + component.setProductDescription(catalogItem.getShortDescription()); + + source.getParameters().forEach( + param -> component.putParamsItem(param.getName(), param.getValues()) + ); + return component; + } default List mapCreateComponentRequestToCreateComponentParameterList(CreateComponentRequest createComponentRequest) { if (createComponentRequest == null) { diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/StatusMap.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/StatusMap.java new file mode 100644 index 0000000..09d8493 --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/StatusMap.java @@ -0,0 +1,23 @@ +package org.opendevstack.apiservice.project.mapper; + +import org.opendevstack.apiservice.project.model.ComponentsStatusDTO; + +import java.util.HashMap; +import java.util.Map; + +public class StatusMap { + + static Map STATUS_MAP = new HashMap<>(); + + static { + STATUS_MAP.put("CREATING", ComponentsStatusDTO.RUNNING); + STATUS_MAP.put("CREATED", ComponentsStatusDTO.READY); + STATUS_MAP.put("FAILED", ComponentsStatusDTO.FAILED); + STATUS_MAP.put("DELETING", ComponentsStatusDTO.DELETING); + STATUS_MAP.put("UNKNOWN", ComponentsStatusDTO.UNKNOWN); + } + + static ComponentsStatusDTO toOldStatus(String status) { + return STATUS_MAP.get(status); + } +} 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 fc9a465..abc9c54 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 @@ -9,7 +9,8 @@ import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; import org.opendevstack.apiservice.project.exception.ComponentAlreadyExistsException; import org.opendevstack.apiservice.project.exception.ComponentCreationException; @@ -25,9 +26,11 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.opendevstack.apiservice.project.util.TestObjectsBuilder.buildTestCatalogItem; import static org.opendevstack.apiservice.project.util.TestObjectsBuilder.buildTestCreateComponentRequest; import static org.opendevstack.apiservice.project.util.TestObjectsBuilder.buildTestMarketplaceComponent; @@ -56,10 +59,13 @@ void tearDown() throws Exception { @Test void get_project_component_returns_mapped_component_when_marketplace_returns_data() throws MarketplaceException { - ProjectComponentInfo marketplaceComponent = buildTestMarketplaceComponent(); + ProjectComponentExtendedInfo marketplaceComponent = buildTestMarketplaceComponent(); + CatalogItem testCatalogItem = buildTestCatalogItem(); when(marketplaceExternalService.getProjectComponent("testProject", "testComponent")) .thenReturn(marketplaceComponent); + when(marketplaceExternalService.getCatalogItem(anyString())) + .thenReturn(testCatalogItem); Component retrievedComponent = componentsFacade.getProjectComponent("testProject", "testComponent"); 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 1abb7b2..a9c059f 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.model.ProjectComponent; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.ComponentsStatusDTO; import org.opendevstack.apiservice.project.model.CreateComponentRequest; @@ -26,15 +26,24 @@ public static Component buildTestComponent() { return component; } - public static ProjectComponentInfo buildTestMarketplaceComponent() { - ProjectComponentInfo component = new ProjectComponentInfo(); + public static ProjectComponentExtendedInfo buildTestMarketplaceComponent() { + ProjectComponentExtendedInfo component = new ProjectComponentExtendedInfo(); component.setComponentId(UUID.randomUUID().toString()); - component.setStatus("RUNNING"); - component.setCanBeDeleted(false); + component.setStatus("CREATING"); component.setComponentUrl("http://test.component.url"); + component.setCatalogItemId("cHJvamVjdHMvVEVTVC9yZXBvcy9DYXRhbG9nSXRlbS55YW1s"); + component.setCatalogItemRef("P2F0PXJlZnMvaGVhZHMvbWFzdGVy"); return component; } + public static CatalogItem buildTestCatalogItem() { + CatalogItem catalogItem = new CatalogItem(); + catalogItem.setId(UUID.randomUUID().toString()); + catalogItem.setTitle("Test Catalog Item"); + catalogItem.setShortDescription("This is a test catalog item"); + return catalogItem; + } + public static CreateComponentRequest buildTestCreateComponentRequest() { CreateComponentRequest request = new CreateComponentRequest(); request.setName("testcomponent"); diff --git a/external-service-marketplace/pom.xml b/external-service-marketplace/pom.xml index a37e94b..0cb68ca 100644 --- a/external-service-marketplace/pom.xml +++ b/external-service-marketplace/pom.xml @@ -150,7 +150,7 @@ generate - FILTER=operationId:getProjectComponents|getProjectComponentById + FILTER=operationId:getProjectComponents|getProjectComponentById|getCatalogItemById java ${project.basedir} resttemplate 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 new file mode 100644 index 0000000..4d5331c --- /dev/null +++ b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/CatalogItemOperations.java @@ -0,0 +1,25 @@ +package org.opendevstack.apiservice.externalservice.marketplace.service; + +import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; + +import java.util.Base64; + +public class CatalogItemOperations { + + public static byte[] encodeId(String id) { + return Base64.getUrlEncoder().encode(id.getBytes()); + } + + public static byte[] decodeId(String id) { + return Base64.getUrlDecoder().decode(id); + } + + public static String buildCatalogItemId(ProjectComponentExtendedInfo component) { + if (component == null || component.getCatalogItemId() == null || component.getCatalogItemRef() == null) { + return null; + } + return new String(encodeId( + new String(decodeId(component.getCatalogItemId())) + new String(decodeId(component.getCatalogItemRef())))); + } +} 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 28f74f8..bc284dd 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,7 +2,8 @@ import org.opendevstack.apiservice.externalservice.api.ExternalService; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; +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 java.util.List; @@ -16,9 +17,13 @@ public interface MarketplaceService extends ExternalService { String getDefaultInstance() throws MarketplaceException; - ProjectComponentInfo getProjectComponent(String projectId, String componentId) throws MarketplaceException; + ProjectComponentExtendedInfo getProjectComponent(String projectId, String componentId) throws MarketplaceException; - ProjectComponentInfo getProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException; + ProjectComponentExtendedInfo getProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException; + + CatalogItem getCatalogItem(String catalogItemId) throws MarketplaceException; + + CatalogItem getCatalogItem(String instanceName, String catalogItemId) throws MarketplaceException; boolean provisionProjectComponent(String projectId, 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 67767d6..c200d63 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 @@ -6,12 +6,14 @@ 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.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.model.CatalogItem; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CreateIncidentAction; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.NotifyProvisioningStatusUpdateRequest; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentInfo; +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; @@ -34,27 +36,50 @@ public MarketplaceServiceImpl(MarketplaceApiClientFactory clientFactory) { log.info("MarketplaceServiceImpl initialized"); } + + @Override + public CatalogItem getCatalogItem(String catalogItemId) throws MarketplaceException { + return getCatalogItem(getDefaultInstance(), catalogItemId); + } + + @Override + public CatalogItem getCatalogItem(String instanceName, String catalogItemId) throws MarketplaceException { + log.debug("Marketplace service GET catalog item with id {} in instance {} ", catalogItemId, instanceName); + try { + MarketplaceApiClient marketplaceClient = clientFactory.getClient(instanceName); + ApiClient apiClient = marketplaceClient.getApiClient(); + CatalogItemsApi catalogItemsApi = new CatalogItemsApi(apiClient); + apiClient.setBasePath(marketplaceClient.getConfig().getProjectComponentsBaseUrl()); + return catalogItemsApi.getCatalogItemById(catalogItemId); + } catch (HttpClientErrorException.NotFound e) { + log.debug("Catalog item with id '{}' not found in Marketplace instance '{}'", + catalogItemId, instanceName); + return null; + } catch (HttpClientErrorException.Unauthorized | HttpClientErrorException.Forbidden e) { + throw new MarketplaceException( + String.format("Access denied when getting catalog item '%s' in instance '%s'", + catalogItemId, instanceName), e); + } catch (RestClientException e) { + throw new MarketplaceException( + String.format("Failed to retrieve catalog item '%s' in instance '%s'", + catalogItemId, instanceName), e); + } + } + @Override - public ProjectComponentInfo getProjectComponent(String projectId, String componentId) throws MarketplaceException { + public ProjectComponentExtendedInfo getProjectComponent(String projectId, String componentId) throws MarketplaceException { return getProjectComponent(getDefaultInstance(), projectId, componentId); } @Override - public ProjectComponentInfo getProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException { + public ProjectComponentExtendedInfo getProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException { log.debug("Marketplace service GET component with id {} for project {} in instance {} ", componentId, projectId, instanceName); try { MarketplaceApiClient marketplaceClient = clientFactory.getClient(instanceName); ApiClient apiClient = marketplaceClient.getApiClient(); ProjectComponentsApi projectComponentsApi = new ProjectComponentsApi(apiClient); apiClient.setBasePath(marketplaceClient.getConfig().getProjectComponentsBaseUrl()); - List components = projectComponentsApi.getProjectComponents(projectId); - if (components == null || components.isEmpty()) { - return null; - } - return components.stream() - .filter(component -> component.getComponentId().equals(componentId)) - .findFirst() - .orElse(null); + return projectComponentsApi.getProjectComponentById(projectId, componentId); } catch (HttpClientErrorException.NotFound e) { log.debug("Component with id '{}' not found in Marketplace instance '{}' for project '{}'", componentId, instanceName, projectId); 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 new file mode 100644 index 0000000..74bce9f --- /dev/null +++ b/external-service-marketplace/src/test/java/org/opendevstack/apiservice/externalservice/marketplace/service/CatalogItemOperationsTest.java @@ -0,0 +1,39 @@ +package org.opendevstack.apiservice.externalservice.marketplace.service; + +import org.junit.jupiter.api.Test; +import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class CatalogItemOperationsTest { + + @Test + void testBuildCatalogItemId_whenNullComponent_ReturnNull() { + String catalogItemId = CatalogItemOperations.buildCatalogItemId(null); + assertNull(catalogItemId); + } + + @Test + void testBuildCatalogItemId_whenNullValues_ReturnNull() { + ProjectComponentExtendedInfo testComponentExtendedInfo = new ProjectComponentExtendedInfo(); + testComponentExtendedInfo.setCatalogItemId(null); + testComponentExtendedInfo.setCatalogItemRef(null); + + String catalogItemId = CatalogItemOperations.buildCatalogItemId(testComponentExtendedInfo); + assertNull(catalogItemId); + } + + @Test + void testBuildCatalogItemId_whenCorrectValues_ReturnCorrectlyBuiltId() { + ProjectComponentExtendedInfo testComponentExtendedInfo = new ProjectComponentExtendedInfo(); + testComponentExtendedInfo.setCatalogItemId("cHJvamVjdHMvVEVTVC9yZXBvcy9DYXRhbG9nSXRlbS55YW1s"); + testComponentExtendedInfo.setCatalogItemRef("P2F0PXJlZnMvaGVhZHMvbWFzdGVy"); + + String catalogItemId = CatalogItemOperations.buildCatalogItemId(testComponentExtendedInfo); + assertNotNull(catalogItemId); + assertEquals("cHJvamVjdHMvVEVTVC9yZXBvcy9DYXRhbG9nSXRlbS55YW1sP2F0PXJlZnMvaGVhZHMvbWFzdGVy", catalogItemId); + } + +} \ No newline at end of file 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 78b73fd..c521ad8 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 @@ -10,6 +10,8 @@ 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.ProjectComponentInfo; import org.opendevstack.apiservice.externalservice.marketplace.service.impl.MarketplaceServiceImpl; import org.springframework.http.HttpHeaders; @@ -27,6 +29,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -131,7 +134,7 @@ void testGetProjectComponent_NotFound_ReturnsNull() throws MarketplaceException when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); // Act - ProjectComponentInfo result = marketplaceService.getProjectComponent(instanceName, projectKey, componentId); + ProjectComponentExtendedInfo result = marketplaceService.getProjectComponent(instanceName, projectKey, componentId); // Assert assertNull(result); @@ -239,7 +242,7 @@ void testGetProjectComponent_NoInstanceArg_UsesDefaultClient() throws Marketplac .thenReturn(ResponseEntity.ok(null)); when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); - ProjectComponentInfo result = marketplaceService.getProjectComponent(projectKey, componentId); + ProjectComponentExtendedInfo result = marketplaceService.getProjectComponent(projectKey, componentId); assertNull(result); verify(clientFactory).getClient(null); @@ -259,7 +262,7 @@ void testGetProjectComponent_NullInstanceName_UsesDefaultClient() throws Marketp .thenReturn(ResponseEntity.ok(null)); when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); - ProjectComponentInfo result = marketplaceService.getProjectComponent(null, projectKey, componentId); + ProjectComponentExtendedInfo result = marketplaceService.getProjectComponent(null, projectKey, componentId); assertNull(result); verify(clientFactory).getClient(null); @@ -278,7 +281,7 @@ void testGetProjectComponent_BlankInstanceName_UsesDefaultClient() throws Market .thenReturn(ResponseEntity.ok(null)); when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); - ProjectComponentInfo result = marketplaceService.getProjectComponent("", projectKey, componentId); + ProjectComponentExtendedInfo result = marketplaceService.getProjectComponent("", projectKey, componentId); assertNull(result); verify(clientFactory).getClient(""); @@ -300,7 +303,7 @@ void testGetProjectComponent_NoInstanceArg_NotFound_ReturnsFalse() throws Market .thenThrow(notFoundEx); when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); - ProjectComponentInfo result = marketplaceService.getProjectComponent(projectKey, componentId); + ProjectComponentExtendedInfo result = marketplaceService.getProjectComponent(projectKey, componentId); assertNull(result); } @@ -365,4 +368,49 @@ void testProvisionProjectComponent_Conflict_ThrowsMarketplaceExceptionWithDuplic assertEquals("This component name already exists, please choose another name.", exception.getMessage()); } + @Test + void testGetCatalogItem_RestClientException() throws MarketplaceException { + // Arrange + String instanceName = "dev"; + String catalogItemId = "test-catalog-item-base64-string"; + MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); + + when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); + when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); + when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); + when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenThrow(new RestClientException("Connection failed")); + + // Act & Assert + assertThrows(MarketplaceException.class, () -> + marketplaceService.getCatalogItem(instanceName, catalogItemId)); + + verify(clientFactory).getClient(instanceName); + verify(marketplaceApiClient).getApiClient(); + } + + @Test + void testGetCatalogItem_NotFound_ReturnsNull() throws MarketplaceException { + // Arrange + String instanceName = "dev"; + String catalogItemId = "test-catalog-item-base64-string"; + MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); + instanceConfig.setAccessToken("1234"); + HttpClientErrorException notFoundEx = HttpClientErrorException.create( + HttpStatus.NOT_FOUND, "Not Found", HttpHeaders.EMPTY, new byte[0], null); + + 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(notFoundEx); + when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); + + // Act + CatalogItem result = marketplaceService.getCatalogItem(instanceName, catalogItemId); + + // Assert + assertNull(result); + verify(clientFactory).getClient(instanceName); + } + } \ No newline at end of file From 05fa84f0682a8723f4e7b574e1ff399ec072775d Mon Sep 17 00:00:00 2001 From: Angel MP Date: Wed, 29 Apr 2026 15:20:59 +0200 Subject: [PATCH 10/25] =?UTF-8?q?Implement=20On-Behalf-Of=20(OBO)=20token?= =?UTF-8?q?=20exchange=20functionality=20and=20enhance=E2=80=A6=20(#20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit … Marketplace API client with bearer token support --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- api-project-component-v0/pom.xml | 6 + api-project/pom.xml | 6 + .../project/controller/ProjectController.java | 4 +- .../project/util/SecurityUtils.java | 33 ----- application.yaml | 30 ++++ .../core/security/jwt/JwtUtils.java | 56 ++++++++ .../core/security/obo/OboTokenException.java | 12 ++ .../core/security/obo/OboTokenProperties.java | 17 +++ .../core/security/obo/OboTokenResponse.java | 25 ++++ .../core/security/obo/OboTokenService.java | 73 ++++++++++ .../core/security/jwt/JwtUtilsTest.java | 132 ++++++++++++++++++ .../security/obo/OboTokenServiceTest.java | 129 +++++++++++++++++ external-service-marketplace/pom.xml | 7 + .../client/MarketplaceApiClient.java | 27 ++-- .../client/MarketplaceApiClientFactory.java | 30 ++-- .../config/MarketplaceInstanceConfig.java | 30 +--- .../service/impl/MarketplaceServiceImpl.java | 46 ++++-- .../service/MarketplaceServiceImplTest.java | 60 ++++++-- .../persistence/dao/PolicyDaoImpl.java | 2 +- .../AuthorizationPolicyJpaRepository.java | 10 +- 20 files changed, 625 insertions(+), 110 deletions(-) delete mode 100644 api-project/src/main/java/org/opendevstack/apiservice/project/util/SecurityUtils.java create mode 100644 core-security/src/main/java/org/opendevstack/apiservice/core/security/jwt/JwtUtils.java create mode 100644 core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenException.java create mode 100644 core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenProperties.java create mode 100644 core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenResponse.java create mode 100644 core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenService.java create mode 100644 core-security/src/test/java/org/opendevstack/apiservice/core/security/jwt/JwtUtilsTest.java create mode 100644 core-security/src/test/java/org/opendevstack/apiservice/core/security/obo/OboTokenServiceTest.java diff --git a/api-project-component-v0/pom.xml b/api-project-component-v0/pom.xml index 845c6d5..35f3ddf 100644 --- a/api-project-component-v0/pom.xml +++ b/api-project-component-v0/pom.xml @@ -50,6 +50,12 @@ ${project.version} + + org.opendevstack.apiservice + core-security + ${project.version} + + io.jsonwebtoken jjwt-api diff --git a/api-project/pom.xml b/api-project/pom.xml index 3a7962e..7172641 100644 --- a/api-project/pom.xml +++ b/api-project/pom.xml @@ -57,6 +57,12 @@ persistence ${project.version} + + + org.opendevstack.apiservice + core-security + ${project.version} + io.jsonwebtoken diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java index c9bc530..54c3327 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java @@ -3,11 +3,11 @@ import jakarta.validation.Valid; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.core.security.jwt.JwtUtils; import org.opendevstack.apiservice.project.api.ProjectsApi; import org.opendevstack.apiservice.project.facade.ProjectsFacade; import org.opendevstack.apiservice.project.model.CreateProjectRequest; import org.opendevstack.apiservice.project.model.CreateProjectResponse; -import org.opendevstack.apiservice.project.util.SecurityUtils; import org.opendevstack.apiservice.project.validation.ProjectRequestValidator; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -38,7 +38,7 @@ public class ProjectController implements ProjectsApi { @Override public ResponseEntity createProject(@Valid @RequestBody CreateProjectRequest createProjectRequest) { projectRequestValidator.validate(createProjectRequest); - UUID clientId = SecurityUtils.getClientId(); + UUID clientId = JwtUtils.getClientId(); CreateProjectResponse projectResponse = projectsFacade.createProject(createProjectRequest, clientId); projectResponse.setLocation(API_BASE_PATH + "/" + projectResponse.getProjectKey()); diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/util/SecurityUtils.java b/api-project/src/main/java/org/opendevstack/apiservice/project/util/SecurityUtils.java deleted file mode 100644 index bb926a9..0000000 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/util/SecurityUtils.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.opendevstack.apiservice.project.util; - -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; - -import java.util.UUID; - -public class SecurityUtils { - - private SecurityUtils() { - // to avoid instantiation - } - - public static UUID getClientId() { - Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - - if (principal instanceof Jwt jwt) { - String clientId = jwt.getClaimAsString("azp"); - if (clientId == null || clientId.isBlank()) { - clientId = jwt.getClaimAsString("appid"); - } - - if (clientId == null || clientId.isBlank()) { - throw new InvalidBearerTokenException("Client ID not found in token claims"); - } - - return UUID.fromString(clientId); - } else { - throw new InvalidBearerTokenException("Invalid authentication token"); - } - } -} diff --git a/application.yaml b/application.yaml index bfaa024..42ccfbc 100644 --- a/application.yaml +++ b/application.yaml @@ -80,6 +80,13 @@ app: - /actuator/health - /actuator/info - /api/v1/projects/*/platforms + obo: + # Azure AD token endpoint used for On-Behalf-Of token exchange. + token-url: ${OBO_TOKEN_URL:https://login.microsoftonline.com/${AZURE_TENANT_ID:}/oauth2/v2.0/token} + # Client ID of this application's Azure AD app registration. + client-id: ${OBO_CLIENT_ID} + # Client secret for OBO exchange. Must be injected via environment variable or secret store. + client-secret: ${OBO_CLIENT_SECRET} # Spring Boot Actuator configuration. # Restrict these endpoints in production if they expose operational details. @@ -278,3 +285,26 @@ externalservices: projects-info-service: # Base URL of the downstream Projects Info Service consumed by this application. base-url: ${PROJECTS_INFO_SERVICE_BASE_URL:http://localhost:8081} + + jira: + # Name of the default Jira instance. + default-instance: ${JIRA_DEFAULT_INSTANCE:jira} + instances: + jira: + base-url: ${JIRA_JIRA_DEV_BASE_URL:https://jira.example.com} + bearer-token: ${JIRA_JIRA_DEV_BEARER_TOKEN:} + connection-timeout: ${JIRA_JIRA_DEV_CONNECTION_TIMEOUT:30000} + read-timeout: ${JIRA_JIRA_DEV_READ_TIMEOUT:30000} + trust-all-certificates: ${JIRA_JIRA_DEV_TRUST_ALL:false} + + marketplace: + instances: + default: + project-components-base-url: ${MARKETPLACE_PROJECT_COMPONENTS_BASE_URL} + provisioner-actions-base-url: ${MARKETPLACE_PROVISIONER_ACTIONS_BASE_URL:} + obo-scope: ${MARKETPLACE_OBO_SCOPE:} + trust-all-certificates: ${MARKETPLACE_TRUST_ALL_CERTS:true} + workflow: ${MARKETPLACE_WORKFLOW:} + ods-namespace: ${MARKETPLACE_ODS_NAMESPACE:} + quickstarter-repository: ${MARKETPLACE_QUICKSTARTER_REPO:} + catalog-item-id: ${MARKETPLACE_CATALOG_ITEM_ID:} diff --git a/core-security/src/main/java/org/opendevstack/apiservice/core/security/jwt/JwtUtils.java b/core-security/src/main/java/org/opendevstack/apiservice/core/security/jwt/JwtUtils.java new file mode 100644 index 0000000..09ebfce --- /dev/null +++ b/core-security/src/main/java/org/opendevstack/apiservice/core/security/jwt/JwtUtils.java @@ -0,0 +1,56 @@ +package org.opendevstack.apiservice.core.security.jwt; + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; + +import java.util.UUID; + +public final class JwtUtils { + + private JwtUtils() { + } + + /** + * Extracts the raw JWT token value from the current SecurityContext. + * + * @return the JWT token string + * @throws InvalidBearerTokenException if the principal is not a JWT + */ + public static String getTokenValue() { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (principal instanceof Jwt jwt) { + return jwt.getTokenValue(); + } + throw new InvalidBearerTokenException("Invalid authentication token"); + } + + /** + * Extracts the Azure AD client ID from the current SecurityContext JWT. + * Checks the {@code azp} claim first, then falls back to {@code appid}. + * + * @return the client ID as UUID + * @throws InvalidBearerTokenException if the principal is not a JWT or no client ID claim is found + */ + public static UUID getClientId() { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (principal instanceof Jwt jwt) { + return extractClientId(jwt); + } + throw new InvalidBearerTokenException("Invalid authentication token"); + } + + /** + * Extracts the Azure AD client ID from the given JWT. + */ + public static UUID extractClientId(Jwt jwt) { + String clientId = jwt.getClaimAsString("azp"); + if (clientId == null || clientId.isBlank()) { + clientId = jwt.getClaimAsString("appid"); + } + if (clientId == null || clientId.isBlank()) { + throw new InvalidBearerTokenException("Client ID not found in token claims"); + } + return UUID.fromString(clientId); + } +} diff --git a/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenException.java b/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenException.java new file mode 100644 index 0000000..bbb797a --- /dev/null +++ b/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenException.java @@ -0,0 +1,12 @@ +package org.opendevstack.apiservice.core.security.obo; + +public class OboTokenException extends RuntimeException { + + public OboTokenException(String message) { + super(message); + } + + public OboTokenException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenProperties.java b/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenProperties.java new file mode 100644 index 0000000..0454b89 --- /dev/null +++ b/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenProperties.java @@ -0,0 +1,17 @@ +package org.opendevstack.apiservice.core.security.obo; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Configuration +@ConfigurationProperties(prefix = "app.security.obo") +@Data +public class OboTokenProperties { + + private String tokenUrl; + + private String clientId; + + private String clientSecret; +} diff --git a/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenResponse.java b/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenResponse.java new file mode 100644 index 0000000..5894738 --- /dev/null +++ b/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenResponse.java @@ -0,0 +1,25 @@ +package org.opendevstack.apiservice.core.security.obo; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@JsonIgnoreProperties(ignoreUnknown = true) +public class OboTokenResponse { + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("token_type") + private String tokenType; + + @JsonProperty("expires_in") + private int expiresIn; + + private String scope; +} diff --git a/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenService.java b/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenService.java new file mode 100644 index 0000000..6bdcc96 --- /dev/null +++ b/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenService.java @@ -0,0 +1,73 @@ +package org.opendevstack.apiservice.core.security.obo; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +@Service +@Slf4j +public class OboTokenService { + + private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + + private final OboTokenProperties properties; + private final RestTemplate restTemplate; + + @Autowired + public OboTokenService(OboTokenProperties properties, RestTemplate restTemplate) { + this.properties = properties; + this.restTemplate = restTemplate; + } + + OboTokenService(OboTokenProperties properties, RestTemplate restTemplate) { + this.properties = properties; + this.restTemplate = restTemplate; + } + + /** + * Exchanges the given JWT assertion for an OBO (On-Behalf-Of) access token. + * + * @param assertion the incoming JWT token value (from the original request) + * @param scope the target API scope (e.g. {@code api:///Api.Access}) + * @return the OBO access token string + * @throws OboTokenException if the token exchange fails + */ + public String exchangeToken(String assertion, String scope) { + log.debug("Exchanging JWT for OBO token with scope '{}'", scope); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("grant_type", GRANT_TYPE); + body.add("client_id", properties.getClientId()); + body.add("client_secret", properties.getClientSecret()); + body.add("assertion", assertion); + body.add("requested_token_use", "on_behalf_of"); + body.add("scope", scope); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + HttpEntity> request = new HttpEntity<>(body, headers); + + try { + ResponseEntity response = + restTemplate.postForEntity(properties.getTokenUrl(), request, OboTokenResponse.class); + + if (response.getBody() == null || response.getBody().getAccessToken() == null) { + throw new OboTokenException("OBO token response body or access_token is null"); + } + + log.debug("OBO token obtained successfully, expires in {} seconds", response.getBody().getExpiresIn()); + return response.getBody().getAccessToken(); + } catch (RestClientException e) { + throw new OboTokenException("Failed to exchange JWT for OBO token: " + e.getMessage(), e); + } + } +} diff --git a/core-security/src/test/java/org/opendevstack/apiservice/core/security/jwt/JwtUtilsTest.java b/core-security/src/test/java/org/opendevstack/apiservice/core/security/jwt/JwtUtilsTest.java new file mode 100644 index 0000000..76903b4 --- /dev/null +++ b/core-security/src/test/java/org/opendevstack/apiservice/core/security/jwt/JwtUtilsTest.java @@ -0,0 +1,132 @@ +package org.opendevstack.apiservice.core.security.jwt; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.InvalidBearerTokenException; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import java.time.Instant; +import java.util.Map; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class JwtUtilsTest { + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void get_token_value_returns_jwt_token_string() { + // GIVEN + Jwt jwt = buildJwt("eyJhbGciOiJSUzI1NiJ9.test", Map.of("azp", "client-a")); + setSecurityContext(jwt); + + // WHEN + String token = JwtUtils.getTokenValue(); + + // THEN + assertEquals("eyJhbGciOiJSUzI1NiJ9.test", token); + } + + @Test + void get_token_value_throws_when_principal_is_not_jwt() { + // GIVEN + SecurityContext ctx = mock(SecurityContext.class); + var auth = mock(org.springframework.security.core.Authentication.class); + when(auth.getPrincipal()).thenReturn("not-a-jwt"); + when(ctx.getAuthentication()).thenReturn(auth); + SecurityContextHolder.setContext(ctx); + + // WHEN / THEN + assertThrows(InvalidBearerTokenException.class, JwtUtils::getTokenValue); + } + + @Test + void get_client_id_returns_azp_claim() { + // GIVEN + String clientId = "00000000-0000-0000-0000-000000000001"; + Jwt jwt = buildJwt("token", Map.of("azp", clientId)); + setSecurityContext(jwt); + + // WHEN + UUID result = JwtUtils.getClientId(); + + // THEN + assertEquals(UUID.fromString(clientId), result); + } + + @Test + void get_client_id_falls_back_to_appid_when_azp_is_blank() { + // GIVEN + String clientId = "00000000-0000-0000-0000-000000000002"; + Jwt jwt = buildJwt("token", Map.of("azp", "", "appid", clientId)); + setSecurityContext(jwt); + + // WHEN + UUID result = JwtUtils.getClientId(); + + // THEN + assertEquals(UUID.fromString(clientId), result); + } + + @Test + void get_client_id_throws_when_no_client_claim() { + // GIVEN + Jwt jwt = buildJwt("token", Map.of("sub", "user")); + setSecurityContext(jwt); + + // WHEN / THEN + assertThrows(InvalidBearerTokenException.class, JwtUtils::getClientId); + } + + @Test + void extract_client_id_from_jwt_with_azp() { + // GIVEN + String clientId = "00000000-0000-0000-0000-000000000003"; + Jwt jwt = buildJwt("token", Map.of("azp", clientId)); + + // WHEN + UUID result = JwtUtils.extractClientId(jwt); + + // THEN + assertEquals(UUID.fromString(clientId), result); + } + + @Test + void extract_client_id_from_jwt_with_appid_fallback() { + // GIVEN + String clientId = "00000000-0000-0000-0000-000000000004"; + Jwt jwt = buildJwt("token", Map.of("appid", clientId)); + + // WHEN + UUID result = JwtUtils.extractClientId(jwt); + + // THEN + assertEquals(UUID.fromString(clientId), result); + } + + private Jwt buildJwt(String tokenValue, Map claims) { + Jwt.Builder builder = Jwt.withTokenValue(tokenValue) + .header("alg", "RS256") + .issuedAt(Instant.now()) + .expiresAt(Instant.now().plusSeconds(3600)); + claims.forEach(builder::claim); + return builder.build(); + } + + private void setSecurityContext(Jwt jwt) { + JwtAuthenticationToken auth = new JwtAuthenticationToken(jwt); + SecurityContext ctx = SecurityContextHolder.createEmptyContext(); + ctx.setAuthentication(auth); + SecurityContextHolder.setContext(ctx); + } +} diff --git a/core-security/src/test/java/org/opendevstack/apiservice/core/security/obo/OboTokenServiceTest.java b/core-security/src/test/java/org/opendevstack/apiservice/core/security/obo/OboTokenServiceTest.java new file mode 100644 index 0000000..ad9d0ca --- /dev/null +++ b/core-security/src/test/java/org/opendevstack/apiservice/core/security/obo/OboTokenServiceTest.java @@ -0,0 +1,129 @@ +package org.opendevstack.apiservice.core.security.obo; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class OboTokenServiceTest { + + @Mock + private RestTemplate restTemplate; + + private OboTokenProperties properties; + + private OboTokenService sut; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + properties = new OboTokenProperties(); + properties.setTokenUrl("https://login.microsoftonline.com/tenant-id/oauth2/v2.0/token"); + properties.setClientId("test-client-id"); + properties.setClientSecret("test-client-secret"); + sut = new OboTokenService(properties, restTemplate); + } + + @Test + void exchange_token_returns_access_token_on_success() { + // GIVEN + String assertion = "jwt-assertion-value"; + String scope = "api://target-app/Api.Access"; + OboTokenResponse tokenResponse = new OboTokenResponse(); + tokenResponse.setAccessToken("obo-access-token"); + tokenResponse.setTokenType("Bearer"); + tokenResponse.setExpiresIn(3600); + tokenResponse.setScope(scope); + + when(restTemplate.postForEntity(eq(properties.getTokenUrl()), any(HttpEntity.class), eq(OboTokenResponse.class))) + .thenReturn(new ResponseEntity<>(tokenResponse, HttpStatus.OK)); + + // WHEN + String result = sut.exchangeToken(assertion, scope); + + // THEN + assertEquals("obo-access-token", result); + verify(restTemplate).postForEntity(eq(properties.getTokenUrl()), any(HttpEntity.class), eq(OboTokenResponse.class)); + } + + @Test + @SuppressWarnings("unchecked") + void exchange_token_sends_correct_form_parameters() { + // GIVEN + String assertion = "my-jwt"; + String scope = "api://app/scope"; + OboTokenResponse tokenResponse = new OboTokenResponse(); + tokenResponse.setAccessToken("token"); + + ArgumentCaptor>> captor = ArgumentCaptor.forClass(HttpEntity.class); + when(restTemplate.postForEntity(anyString(), captor.capture(), eq(OboTokenResponse.class))) + .thenReturn(new ResponseEntity<>(tokenResponse, HttpStatus.OK)); + + // WHEN + sut.exchangeToken(assertion, scope); + + // THEN + MultiValueMap body = captor.getValue().getBody(); + assertNotNull(body); + assertEquals("urn:ietf:params:oauth:grant-type:jwt-bearer", body.getFirst("grant_type")); + assertEquals("test-client-id", body.getFirst("client_id")); + assertEquals("test-client-secret", body.getFirst("client_secret")); + assertEquals("my-jwt", body.getFirst("assertion")); + assertEquals("on_behalf_of", body.getFirst("requested_token_use")); + assertEquals("api://app/scope", body.getFirst("scope")); + } + + @Test + void exchange_token_throws_obo_exception_when_response_body_is_null() { + // GIVEN + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(OboTokenResponse.class))) + .thenReturn(new ResponseEntity<>(null, HttpStatus.OK)); + + // WHEN / THEN + OboTokenException ex = assertThrows(OboTokenException.class, + () -> sut.exchangeToken("jwt", "scope")); + assertTrue(ex.getMessage().contains("null")); + } + + @Test + void exchange_token_throws_obo_exception_when_access_token_is_null() { + // GIVEN + OboTokenResponse response = new OboTokenResponse(); + response.setAccessToken(null); + + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(OboTokenResponse.class))) + .thenReturn(new ResponseEntity<>(response, HttpStatus.OK)); + + // WHEN / THEN + assertThrows(OboTokenException.class, () -> sut.exchangeToken("jwt", "scope")); + } + + @Test + void exchange_token_throws_obo_exception_on_rest_client_error() { + // GIVEN + when(restTemplate.postForEntity(anyString(), any(HttpEntity.class), eq(OboTokenResponse.class))) + .thenThrow(new RestClientException("Connection refused")); + + // WHEN / THEN + OboTokenException ex = assertThrows(OboTokenException.class, + () -> sut.exchangeToken("jwt", "scope")); + assertTrue(ex.getMessage().contains("Connection refused")); + } +} diff --git a/external-service-marketplace/pom.xml b/external-service-marketplace/pom.xml index 0cb68ca..d59b591 100644 --- a/external-service-marketplace/pom.xml +++ b/external-service-marketplace/pom.xml @@ -21,6 +21,13 @@ ${project.version} + + + org.opendevstack.apiservice + core-security + ${project.version} + + org.springframework.boot 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 9fd4642..7796f68 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,21 +37,7 @@ public MarketplaceApiClient(String instanceName, MarketplaceInstanceConfig confi // Initialize the generated ApiClient this.apiClient = new ApiClient(restTemplate); - // Configure authentication – prefer bearer token over basic auth - if (config.getBearerToken() != null && !config.getBearerToken().isEmpty()) { - HttpBearerAuth auth = (HttpBearerAuth) this.apiClient.getAuthentication("bearerAuth"); - auth.setBearerToken(config.getBearerToken()); - log.info("MarketplaceApiClient initialized for instance '{}' with bearer token authentication", - instanceName); - } else if (config.getUsername() != null && config.getPassword() != null) { - this.apiClient.setUsername(config.getUsername()); - this.apiClient.setPassword(config.getPassword()); - log.info("MarketplaceApiClient initialized for instance '{}' with basic authentication", - instanceName); - } else { - log.warn("MarketplaceApiClient initialized for instance '{}' without authentication " - + "(neither bearer token nor username/password provided)", instanceName); - } + log.info("MarketplaceApiClient initialized for instance '{}'", instanceName); } /** @@ -70,4 +56,15 @@ private void configureRestTemplateWithJsonNullable(RestTemplate restTemplate) { } log.warn("No MappingJackson2HttpMessageConverter found in RestTemplate for instance '{}'", instanceName); } + + /** + * Sets the bearer token for authentication on this client. + * This replaces any previously configured bearer token. + * + * @param bearerToken the bearer token to use for subsequent API calls + */ + public void setBearerToken(String bearerToken) { + HttpBearerAuth auth = (HttpBearerAuth) this.apiClient.getAuthentication("bearerAuth"); + auth.setBearerToken(bearerToken); + } } 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 70cc2d0..9555c47 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 @@ -5,8 +5,6 @@ import org.opendevstack.apiservice.externalservice.marketplace.config.MarketplaceServiceConfig; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; import org.springframework.boot.web.client.RestTemplateBuilder; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; import org.springframework.http.client.SimpleClientHttpRequestFactory; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @@ -71,13 +69,14 @@ public String getDefaultInstanceName() throws MarketplaceException { /** * Get a {@link MarketplaceApiClient} for a specific instance. - * If {@code instanceName} is {@code null} or blank, the default instance is used. + * 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. * * @param instanceName Name of the Marketplace instance, or {@code null}/{@code ""} for the default * @return Configured MarketplaceApiClient * @throws MarketplaceException if the instance is not configured */ - @Cacheable(value = "marketplaceApiClients", key = "#instanceName", condition = "#instanceName != null && !#instanceName.isBlank()") public MarketplaceApiClient getClient(String instanceName) throws MarketplaceException { if (instanceName == null || instanceName.isBlank()) { throw new MarketplaceException( @@ -99,6 +98,20 @@ public MarketplaceApiClient getClient(String instanceName) throws MarketplaceExc return new MarketplaceApiClient(instanceName, instanceConfig, restTemplate); } + /** + * Get a {@link MarketplaceApiClient} for a specific instance with the given bearer token. + * + * @param instanceName Name of the Marketplace instance + * @param bearerToken Bearer token to use for authentication + * @return Configured MarketplaceApiClient with the given bearer token + * @throws MarketplaceException if the instance is not configured + */ + public MarketplaceApiClient getClient(String instanceName, String bearerToken) throws MarketplaceException { + MarketplaceApiClient client = getClient(instanceName); + client.setBearerToken(bearerToken); + return client; + } + /** * Get the default client, as determined by {@code externalservices.marketplace.default-instance}. * Falls back to the first configured instance when {@code default-instance} is not set. @@ -106,7 +119,6 @@ public MarketplaceApiClient getClient(String instanceName) throws MarketplaceExc * @return MarketplaceApiClient for the default instance * @throws MarketplaceException if no instances are configured */ - @Cacheable(value = "marketplaceApiClients", key = "'default'") public MarketplaceApiClient getClient() throws MarketplaceException { String defaultInstanceName = getDefaultInstanceName(); MarketplaceInstanceConfig instanceConfig = configuration.getInstances().get(defaultInstanceName); @@ -134,14 +146,6 @@ public boolean hasInstance(String instanceName) { return configuration.getInstances().containsKey(instanceName); } - /** - * Clear the client cache (useful for testing or when configuration changes). - */ - @CacheEvict(value = "marketplaceApiClients", allEntries = true) - public void clearCache() { - log.info("Clearing MarketplaceApiClient cache"); - } - /** * Create a configured RestTemplate for a Marketplace instance. * 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 0414f20..45ee784 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,28 +14,6 @@ public class MarketplaceInstanceConfig { */ private String provisionerActionsBaseUrl; - /** - * Authentication access token for accessing the Marketplace API - */ - private String accessToken; - - /** - * Authentication bearer token for accessing the Marketplace API - */ - private String bearerToken; - - /** - * Username for authentication (used with password for basic auth). - * Only used if bearerToken is not provided. - */ - private String username; - - /** - * Password or personal access token for authentication. - * Only used if bearerToken is not provided. - */ - private String password; - /** * Connection timeout in milliseconds (default: 30000) */ @@ -50,7 +28,13 @@ public class MarketplaceInstanceConfig { * Whether to trust all SSL certificates (default: false). * WARNING: Should only be used in development environments. */ - private boolean trustAllCertificates = false; + private boolean trustAllCertificates = false; + + /** + * OAuth2 scope used for OBO token exchange when calling this Marketplace instance. + * Example: {@code api:///Api.Access} + */ + private String oboScope; private String workflow; 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 c200d63..61819e2 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 @@ -1,6 +1,8 @@ package org.opendevstack.apiservice.externalservice.marketplace.service.impl; import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.core.security.jwt.JwtUtils; +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.config.MarketplaceInstanceConfig; @@ -30,9 +32,12 @@ public class MarketplaceServiceImpl implements MarketplaceService { private final MarketplaceApiClientFactory clientFactory; + private final OboTokenService oboTokenService; - public MarketplaceServiceImpl(MarketplaceApiClientFactory clientFactory) { + public MarketplaceServiceImpl(MarketplaceApiClientFactory clientFactory, + OboTokenService oboTokenService) { this.clientFactory = clientFactory; + this.oboTokenService = oboTokenService; log.info("MarketplaceServiceImpl initialized"); } @@ -75,7 +80,7 @@ public ProjectComponentExtendedInfo getProjectComponent(String projectId, String public ProjectComponentExtendedInfo getProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException { log.debug("Marketplace service GET component with id {} for project {} in instance {} ", componentId, projectId, instanceName); try { - MarketplaceApiClient marketplaceClient = clientFactory.getClient(instanceName); + MarketplaceApiClient marketplaceClient = getAuthenticatedClient(instanceName); ApiClient apiClient = marketplaceClient.getApiClient(); ProjectComponentsApi projectComponentsApi = new ProjectComponentsApi(apiClient); apiClient.setBasePath(marketplaceClient.getConfig().getProjectComponentsBaseUrl()); @@ -104,12 +109,11 @@ public boolean provisionProjectComponent(String projectId, List params) throws MarketplaceException { log.debug("Marketplace service PROVISION component for project {}: ", projectId); try { - MarketplaceApiClient marketplaceClient = clientFactory.getClient(instanceName); + MarketplaceApiClient marketplaceClient = getAuthenticatedClient(instanceName); ApiClient apiClient = marketplaceClient.getApiClient(); MarketplaceInstanceConfig config = marketplaceClient.getConfig(); String provisionerActionsBaseUrl = config.getProvisionerActionsBaseUrl(); - String accessToken = config.getAccessToken(); String workflow = config.getWorkflow(); String odsNamespace = config.getOdsNamespace(); String quickstarterRepository = config.getQuickstarterRepository(); @@ -118,10 +122,6 @@ public boolean provisionProjectComponent(String instanceName, String projectId, ProvisionAction provisionAction = new ProvisionAction(); provisionAction.setId("PROVISION"); provisionAction.addParametersItem(new ProvisionActionParameter().name("project_key").type("string").value(projectId)); - - //TODO: currently in dev is not working but, we need to add - //TODO: the error 500 on POST request for "https://component-provisioner-devstack-dev.apps.eu-dev.ocp.aws.boehringer.com/v1/provision-actions": "{"message":"Duplicate key access_token (attempted merging values [...]])"}" -// provisionAction.addParametersItem(new ProvisionActionParameter().name("access_token").type("string").value(accessToken)); provisionAction.addParametersItem(new ProvisionActionParameter().name("workflow").type("string").value(workflow)); provisionAction.addParametersItem(new ProvisionActionParameter().name("ods_namespace").type("string").value(odsNamespace)); provisionAction.addParametersItem(new ProvisionActionParameter().name("quickstarter_repo").type("string").value(quickstarterRepository)); @@ -156,7 +156,7 @@ public boolean deleteProjectComponent(String projectId, String componentId) thro public boolean deleteProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException { log.debug("Marketplace service DELETE component {} for project {}: ", componentId, projectId); try { - MarketplaceApiClient marketplaceClient = clientFactory.getClient(instanceName); + MarketplaceApiClient marketplaceClient = getAuthenticatedClient(instanceName); ApiClient apiClient = marketplaceClient.getApiClient(); ProvisionResultsApi provisionResultsApi = new ProvisionResultsApi(apiClient); @@ -186,7 +186,7 @@ public void registerProjectComponent(String projectId, String componentId) throw public void registerProjectComponent(String instanceName, String projectId, String componentId) throws MarketplaceException { log.debug("Marketplace service REGISTER component {} for project {}: ", componentId, projectId); try { - MarketplaceApiClient marketplaceClient = clientFactory.getClient(instanceName); + MarketplaceApiClient marketplaceClient = getAuthenticatedClient(instanceName); ApiClient apiClient = marketplaceClient.getApiClient(); ProvisionResultsApi provisionResultsApi = new ProvisionResultsApi(apiClient); @@ -239,4 +239,30 @@ public Set getAvailableInstances() { public boolean hasInstance(String instanceName) { return clientFactory.hasInstance(instanceName); } + + /** + * Creates a {@link MarketplaceApiClient} authenticated with an OBO token + * obtained from the current request's JWT. + */ + private MarketplaceApiClient getAuthenticatedClient(String instanceName) throws MarketplaceException { + MarketplaceApiClient client = clientFactory.getClient(instanceName); + String oboScope = client.getConfig().getOboScope(); + if (oboScope == null || oboScope.isBlank()) { + throw new MarketplaceException( + String.format("OBO scope not configured for Marketplace instance '%s'", instanceName)); + } + String assertion = JwtUtils.getTokenValue(); + final String oboToken; + try { + oboToken = oboTokenService.exchangeToken(assertion, oboScope); + } catch (RuntimeException ex) { + throw new MarketplaceException( + String.format( + "Failed to exchange OBO token for Marketplace instance '%s' with scope '%s'", + instanceName, oboScope), + ex); + } + client.setBearerToken(oboToken); + return client; + } } 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 c521ad8..131140a 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 @@ -1,10 +1,16 @@ package org.opendevstack.apiservice.externalservice.marketplace.service; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +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.config.MarketplaceInstanceConfig; @@ -23,6 +29,7 @@ import org.springframework.web.client.RestClientException; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.Collections; import java.util.List; import java.util.Set; @@ -57,11 +64,30 @@ class MarketplaceServiceImplTest { @Mock private ApiClient apiClient; + @Mock + private OboTokenService oboTokenService; + private MarketplaceService marketplaceService; @BeforeEach void setUp() { - marketplaceService = new MarketplaceServiceImpl(clientFactory); + marketplaceService = new MarketplaceServiceImpl(clientFactory, oboTokenService); + + // Set up a fake SecurityContext so JwtUtils.getTokenValue() works + Jwt jwt = Jwt.withTokenValue("test-jwt-assertion") + .header("alg", "RS256") + .claim("azp", "test-client-id") + .issuedAt(Instant.now()) + .expiresAt(Instant.now().plusSeconds(3600)) + .build(); + SecurityContext ctx = SecurityContextHolder.createEmptyContext(); + ctx.setAuthentication(new JwtAuthenticationToken(jwt)); + SecurityContextHolder.setContext(ctx); + + // Default OBO stub — tests that fail before OBO won't reach this + lenient().when(oboTokenService.exchangeToken(anyString(), anyString())) + .thenReturn("obo-test-token"); + // Stub ApiClient utility methods used by the generated ProjectApi / ServerInfoApi // before invokeAPI is reached. Without these, putAll(null) causes NullPointerException. lenient().when(apiClient.parameterToMultiValueMap(any(), anyString(), any())) @@ -72,6 +98,11 @@ void setUp() { .thenReturn(MediaType.APPLICATION_JSON); } + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + // ------------------------------------------------------------------------- // getProjectComponent // ------------------------------------------------------------------------- @@ -101,6 +132,7 @@ void testGetProjectComponent_RestClientException() throws MarketplaceException { String projectKey = "PROJ"; String componentId = "test-component"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); + instanceConfig.setOboScope("api://test/scope"); when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); @@ -123,7 +155,7 @@ void testGetProjectComponent_NotFound_ReturnsNull() throws MarketplaceException String projectKey = "UNKNOWN"; String componentId = "test-component"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setAccessToken("1234"); + instanceConfig.setOboScope("api://test/scope"); HttpClientErrorException notFoundEx = HttpClientErrorException.create( HttpStatus.NOT_FOUND, "Not Found", HttpHeaders.EMPTY, new byte[0], null); @@ -234,9 +266,10 @@ void testGetProjectComponent_NoInstanceArg_UsesDefaultClient() throws Marketplac String projectKey = "PROJ"; String componentId = "test-component"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setAccessToken("1234"); + instanceConfig.setOboScope("api://test/scope"); - when(clientFactory.getClient(null)).thenReturn(marketplaceApiClient); + when(clientFactory.getDefaultInstanceName()).thenReturn("default"); + when(clientFactory.getClient("default")).thenReturn(marketplaceApiClient); when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) .thenReturn(ResponseEntity.ok(null)); @@ -245,16 +278,16 @@ void testGetProjectComponent_NoInstanceArg_UsesDefaultClient() throws Marketplac ProjectComponentExtendedInfo result = marketplaceService.getProjectComponent(projectKey, componentId); assertNull(result); - verify(clientFactory).getClient(null); + verify(clientFactory).getClient("default"); } @Test void testGetProjectComponent_NullInstanceName_UsesDefaultClient() throws MarketplaceException { - // Passing null explicitly as instanceName should also resolve to the default + // Passing null explicitly as instanceName should resolve via getDefaultInstance -> getAuthenticatedClient String projectKey = "PROJ"; String componentId = "test-component"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setAccessToken("1234"); + instanceConfig.setOboScope("api://test/scope"); when(clientFactory.getClient(null)).thenReturn(marketplaceApiClient); when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); @@ -273,7 +306,7 @@ void testGetProjectComponent_BlankInstanceName_UsesDefaultClient() throws Market String projectKey = "PROJ"; String componentId = "test-component"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setAccessToken("1234"); + instanceConfig.setOboScope("api://test/scope"); when(clientFactory.getClient("")).thenReturn(marketplaceApiClient); when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); @@ -292,12 +325,13 @@ void testGetProjectComponent_NoInstanceArg_NotFound_ReturnsFalse() throws Market String projectKey = "ZZZNOPE"; String componentId = "test-component"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setAccessToken("1234"); + instanceConfig.setOboScope("api://test/scope"); HttpClientErrorException notFoundEx = HttpClientErrorException.create( HttpStatus.NOT_FOUND, "Not Found", HttpHeaders.EMPTY, new byte[0], null); - when(clientFactory.getClient(null)).thenReturn(marketplaceApiClient); + when(clientFactory.getDefaultInstanceName()).thenReturn("default"); + when(clientFactory.getClient("default")).thenReturn(marketplaceApiClient); when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) .thenThrow(notFoundEx); @@ -311,9 +345,10 @@ void testGetProjectComponent_NoInstanceArg_NotFound_ReturnsFalse() throws Market @Test void testGetProjectComponent_NoInstanceArg_RestClientException_ThrowsMarketplaceException() throws MarketplaceException { MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setAccessToken("1234"); + instanceConfig.setOboScope("api://test/scope"); - when(clientFactory.getClient(null)).thenReturn(marketplaceApiClient); + when(clientFactory.getDefaultInstanceName()).thenReturn("default"); + when(clientFactory.getClient("default")).thenReturn(marketplaceApiClient); when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) .thenThrow(new RestClientException("timeout")); @@ -347,6 +382,7 @@ void testProvisionProjectComponent_Conflict_ThrowsMarketplaceExceptionWithDuplic String projectKey = "EDPC"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); instanceConfig.setProvisionerActionsBaseUrl("https://example/provision-actions"); + instanceConfig.setOboScope("api://test/scope"); HttpClientErrorException conflictEx = HttpClientErrorException.create( HttpStatus.CONFLICT, diff --git a/persistence/src/main/java/org/opendevstack/apiservice/persistence/dao/PolicyDaoImpl.java b/persistence/src/main/java/org/opendevstack/apiservice/persistence/dao/PolicyDaoImpl.java index 408d54b..9b85c97 100644 --- a/persistence/src/main/java/org/opendevstack/apiservice/persistence/dao/PolicyDaoImpl.java +++ b/persistence/src/main/java/org/opendevstack/apiservice/persistence/dao/PolicyDaoImpl.java @@ -41,7 +41,7 @@ public List findByApiDefinitionIdAndClientId(String apiDefinitionId, @Override public List findGlobalByApiDefinitionId(String apiDefinitionId) { - return repository.findByApiDefinitionIdAndClientIdIsNull(apiDefinitionId).stream() + return repository.findGlobalByApiDefinitionId(apiDefinitionId).stream() .map(this::toDto) .toList(); } diff --git a/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/AuthorizationPolicyJpaRepository.java b/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/AuthorizationPolicyJpaRepository.java index 8f71fb7..1df9182 100644 --- a/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/AuthorizationPolicyJpaRepository.java +++ b/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/AuthorizationPolicyJpaRepository.java @@ -1,6 +1,8 @@ package org.opendevstack.apiservice.persistence.repository; import org.opendevstack.apiservice.persistence.entity.AuthorizationPolicyEntity; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; @@ -12,5 +14,11 @@ public interface AuthorizationPolicyJpaRepository extends JpaRepository findByApiDefinitionIdAndClientId(String apiDefinitionId, String clientId); - List findByApiDefinitionIdAndClientIdIsNull(String apiDefinitionId); + @Query(""" + SELECT p + FROM AuthorizationPolicyEntity p + WHERE p.apiDefinitionId = :apiDefinitionId + AND (p.clientId IS NULL OR TRIM(p.clientId) = '') + """) + List findGlobalByApiDefinitionId(@Param("apiDefinitionId") String apiDefinitionId); } From db955b7880a47ca45ae37d4bd5c639faf30a8a9b Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Wed, 29 Apr 2026 15:33:10 +0200 Subject: [PATCH 11/25] Fix github comments. --- .../opendevstack/apiservice/project/mapper/StatusMap.java | 5 ++++- .../apiservice/project/facade/ComponentsFacadeTest.java | 5 +++-- .../apiservice/core/security/obo/OboTokenService.java | 5 ----- .../marketplace/service/CatalogItemOperations.java | 4 +++- .../marketplace/service/MarketplaceServiceImplTest.java | 1 - 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/StatusMap.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/StatusMap.java index 09d8493..cf9816b 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/StatusMap.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/StatusMap.java @@ -7,7 +7,10 @@ public class StatusMap { - static Map STATUS_MAP = new HashMap<>(); + private StatusMap() { + } + + static final Map STATUS_MAP = new HashMap<>(); static { STATUS_MAP.put("CREATING", ComponentsStatusDTO.RUNNING); 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 abc9c54..f72368a 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 @@ -15,6 +15,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.ComponentRetrievalException; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.ComponentsStatusDTO; @@ -81,8 +82,8 @@ void get_project_component_throws_not_found_when_marketplace_returns_null() thro .thenReturn(null); assertThatThrownBy(() -> componentsFacade.getProjectComponent("testProject", "testComponent")) - .isInstanceOf(ComponentNotFoundException.class) - .hasMessage("Component 'testComponent' not found for project 'testProject'"); + .isInstanceOf(ComponentRetrievalException.class) + .hasMessage("Failed to retrieve component 'testComponent' for project 'testProject': Component 'testComponent' not found for project 'testProject'"); verify(marketplaceExternalService).getProjectComponent("testProject", "testComponent"); } diff --git a/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenService.java b/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenService.java index 6bdcc96..6a9a7a2 100644 --- a/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenService.java +++ b/core-security/src/main/java/org/opendevstack/apiservice/core/security/obo/OboTokenService.java @@ -27,11 +27,6 @@ public OboTokenService(OboTokenProperties properties, RestTemplate restTemplate) this.restTemplate = restTemplate; } - OboTokenService(OboTokenProperties properties, RestTemplate restTemplate) { - this.properties = properties; - this.restTemplate = restTemplate; - } - /** * Exchanges the given JWT assertion for an OBO (On-Behalf-Of) access token. * 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 4d5331c..8c51f8a 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,12 +1,14 @@ package org.opendevstack.apiservice.externalservice.marketplace.service; -import lombok.extern.slf4j.Slf4j; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo; import java.util.Base64; public class CatalogItemOperations { + private CatalogItemOperations() { + } + public static byte[] encodeId(String id) { return Base64.getUrlEncoder().encode(id.getBytes()); } 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 131140a..bf8a6aa 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 @@ -431,7 +431,6 @@ void testGetCatalogItem_NotFound_ReturnsNull() throws MarketplaceException { String instanceName = "dev"; String catalogItemId = "test-catalog-item-base64-string"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - instanceConfig.setAccessToken("1234"); HttpClientErrorException notFoundEx = HttpClientErrorException.create( HttpStatus.NOT_FOUND, "Not Found", HttpHeaders.EMPTY, new byte[0], null); From 05a1abfb18c3c67ae5773889859e80480b38216c Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Wed, 29 Apr 2026 15:51:47 +0200 Subject: [PATCH 12/25] Refactor exception handling in getProjectComponent to use ComponentNotFoundException --- .../apiservice/project/facade/ComponentsFacadeTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 f72368a..9ea9784 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 @@ -82,8 +82,8 @@ void get_project_component_throws_not_found_when_marketplace_returns_null() thro .thenReturn(null); assertThatThrownBy(() -> componentsFacade.getProjectComponent("testProject", "testComponent")) - .isInstanceOf(ComponentRetrievalException.class) - .hasMessage("Failed to retrieve component 'testComponent' for project 'testProject': Component 'testComponent' not found for project 'testProject'"); + .isInstanceOf(ComponentNotFoundException.class) + .hasMessage("Component 'testComponent' not found for project 'testProject'"); verify(marketplaceExternalService).getProjectComponent("testProject", "testComponent"); } From 3a7c3729f1f5d821faf7dc59814b6afa247dbf21 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Wed, 29 Apr 2026 16:21:04 +0200 Subject: [PATCH 13/25] Add ComponentBadRequestException and enhance exception handling for bad requests --- Makefile | 13 +++++++++ .../controller/ComponentsResponseFactory.java | 4 +++ .../ProjectComponentsExceptionHandler.java | 17 +++++++++++ .../ComponentBadRequestException.java | 12 ++++++++ .../project/facade/ComponentsFacade.java | 28 +++++++++++++++++++ 5 files changed, 74 insertions(+) create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentBadRequestException.java diff --git a/Makefile b/Makefile index 79b4438..7620f72 100644 --- a/Makefile +++ b/Makefile @@ -91,6 +91,19 @@ check-maven: clean: check-maven @echo "$(BLUE)Cleaning build artifacts...$(NC)" $(MAVEN_WRAPPER) clean + @echo "$(BLUE)Removing all target directories across modules...$(NC)" + @find . -type d -name target -prune -exec rm -rf {} + + @echo "$(BLUE)Removing generated OpenAPI model directories...$(NC)" + @find . -type d -path "*/src/main/java/*/model" \ + ! -path "./external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/model" \ + ! -path "./service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model" \ + ! -path "./external-service-projects-info-service/src/main/java/org/opendevstack/apiservice/externalservice/projectsinfoservice/model" \ + ! -path "./external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/model" \ + ! -path "./external-service-uipath/src/main/java/org/opendevstack/apiservice/externalservice/uipath/model" \ + -prune -exec rm -rf {} + + @echo "$(BLUE)Removing local Maven cache for org.opendevstack.apiservice...$(NC)" + @rm -rf "$$HOME/.m2/repository/org/opendevstack/apiservice" + @rm -rf "$$HOME/.m2/repositories/org/opendevstack/apiservice" @echo "$(GREEN)✓ Clean complete$(NC)" ## Compile the project diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java index 9e6cb70..0feb0ad 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ComponentsResponseFactory.java @@ -30,6 +30,10 @@ public static CreateComponentResponse notFound(String path, String message, Comp return buildResponse(HttpStatus.NOT_FOUND, errorKey, path, message); } + public static CreateComponentResponse unprocessableEntity(String path, String message, ComponentErrorKey errorKey) { + return buildResponse(HttpStatus.UNPROCESSABLE_ENTITY, errorKey, path, message); + } + public static CreateComponentResponse internalError(String path, String message, ComponentErrorKey errorKey) { return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, errorKey, path, message); } 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 160e411..f31b069 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 @@ -5,6 +5,7 @@ import org.opendevstack.apiservice.project.controller.ComponentsResponseFactory; import org.opendevstack.apiservice.project.controller.ProjectComponentsController; 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.ComponentErrorKey; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; @@ -167,6 +168,22 @@ public ResponseEntity handleComponentAlreadyExistsExcep return ResponseEntity.status(HttpStatus.CONFLICT).body(response); } + @ExceptionHandler(ComponentBadRequestException.class) + public ResponseEntity handleComponentBadRequestException( + ComponentBadRequestException ex, + HttpServletRequest request) { + + log.warn("Bad request from downstream service: {}", ex.getMessage()); + + CreateComponentResponse response = ComponentsResponseFactory.unprocessableEntity( + request.getRequestURI(), + ex.getMessage(), + ComponentErrorKey.INVALID_PARAMETERS + ); + + return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(response); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleGenericException( Exception ex, diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentBadRequestException.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentBadRequestException.java new file mode 100644 index 0000000..10deaec --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentBadRequestException.java @@ -0,0 +1,12 @@ +package org.opendevstack.apiservice.project.exception; + +public class ComponentBadRequestException extends RuntimeException { + + public ComponentBadRequestException(String message) { + super(message); + } + + public ComponentBadRequestException(String message, Throwable cause) { + super(message, cause); + } +} 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 685a91f..f56451d 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 @@ -10,6 +10,7 @@ import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; import org.opendevstack.apiservice.project.exception.CatalogItemNotFoundException; 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.ComponentNotFoundException; import org.opendevstack.apiservice.project.exception.ComponentRetrievalException; @@ -72,6 +73,10 @@ public void provisionProjectComponent(String projectId, CreateComponentRequest c if (isConflictCause(e)) { throw new ComponentAlreadyExistsException(e.getMessage(), e); } + if (isBadRequestCause(e)) { + String downstreamMessage = extractHttpErrorMessage(e); + throw new ComponentBadRequestException(downstreamMessage, e); + } throw new ComponentCreationException( String.format("Failed to create component for project '%s': %s", projectId, e.getMessage()), e ); @@ -89,6 +94,29 @@ private boolean isConflictCause(Throwable throwable) { return false; } + private boolean isBadRequestCause(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof HttpClientErrorException.UnprocessableEntity + || current instanceof HttpClientErrorException.BadRequest) { + return true; + } + current = current.getCause(); + } + return false; + } + + private String extractHttpErrorMessage(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof HttpClientErrorException httpError) { + return httpError.getResponseBodyAsString(); + } + current = current.getCause(); + } + return throwable.getMessage(); + } + public Boolean deleteProjectComponent(String projectId, String componentId) { try { return marketplaceExternalService.deleteProjectComponent(projectId, componentId); From 2febcc765b9a96902f655a5930ad1beaf7ab0166 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Wed, 29 Apr 2026 18:00:12 +0200 Subject: [PATCH 14/25] Remove unused workflow, odsNamespace, and quickstarterRepository fields from MarketplaceInstanceConfig and MarketplaceServiceImpl --- .../marketplace/config/MarketplaceInstanceConfig.java | 6 ------ .../marketplace/service/impl/MarketplaceServiceImpl.java | 6 ------ 2 files changed, 12 deletions(-) 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 45ee784..465564d 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 @@ -36,11 +36,5 @@ public class MarketplaceInstanceConfig { */ private String oboScope; - private String workflow; - - private String odsNamespace; - - private String quickstarterRepository; - private String catalogItemId; } \ No newline at end of file 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 61819e2..879eb7f 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 @@ -114,17 +114,11 @@ public boolean provisionProjectComponent(String instanceName, String projectId, MarketplaceInstanceConfig config = marketplaceClient.getConfig(); String provisionerActionsBaseUrl = config.getProvisionerActionsBaseUrl(); - String workflow = config.getWorkflow(); - String odsNamespace = config.getOdsNamespace(); - String quickstarterRepository = config.getQuickstarterRepository(); String catalogItemId = config.getCatalogItemId(); ProvisionAction provisionAction = new ProvisionAction(); provisionAction.setId("PROVISION"); provisionAction.addParametersItem(new ProvisionActionParameter().name("project_key").type("string").value(projectId)); - provisionAction.addParametersItem(new ProvisionActionParameter().name("workflow").type("string").value(workflow)); - provisionAction.addParametersItem(new ProvisionActionParameter().name("ods_namespace").type("string").value(odsNamespace)); - provisionAction.addParametersItem(new ProvisionActionParameter().name("quickstarter_repo").type("string").value(quickstarterRepository)); provisionAction.addParametersItem(new ProvisionActionParameter().name("catalog_item_id").type("string").value(catalogItemId)); params.forEach(provisionAction::addParametersItem); From 5dff5754f9b53d3439a18b98ebbb8765520fa6d3 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Wed, 29 Apr 2026 21:33:27 +0200 Subject: [PATCH 15/25] Fix test. --- .../apiservice/project/facade/ComponentsFacadeTest.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) 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 f72368a..abc9c54 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 @@ -15,7 +15,6 @@ 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.ComponentRetrievalException; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.ComponentsStatusDTO; @@ -82,8 +81,8 @@ void get_project_component_throws_not_found_when_marketplace_returns_null() thro .thenReturn(null); assertThatThrownBy(() -> componentsFacade.getProjectComponent("testProject", "testComponent")) - .isInstanceOf(ComponentRetrievalException.class) - .hasMessage("Failed to retrieve component 'testComponent' for project 'testProject': Component 'testComponent' not found for project 'testProject'"); + .isInstanceOf(ComponentNotFoundException.class) + .hasMessage("Component 'testComponent' not found for project 'testProject'"); verify(marketplaceExternalService).getProjectComponent("testProject", "testComponent"); } From 14c7d6bbf03f6dcaf148e9701f197827132f1c3f Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Tue, 5 May 2026 15:33:49 +0200 Subject: [PATCH 16/25] Add delete service and controller. --- .../api-project-component-v0-internal.yaml | 58 +++++++++++++++++++ api-project-component-v0/pom.xml | 29 ++++++++++ .../ProjectComponentsController.java | 11 +++- .../ProjectComponentsExceptionHandler.java | 12 ++++ .../exception/ComponentDeletionException.java | 12 ++++ .../project/facade/ComponentsFacade.java | 10 +++- external-service-marketplace/pom.xml | 9 +-- .../client/MarketplaceApiClient.java | 6 ++ .../config/MarketplaceInstanceConfig.java | 11 ++++ .../service/MarketplaceService.java | 4 +- .../service/impl/MarketplaceServiceImpl.java | 16 ++--- 11 files changed, 155 insertions(+), 23 deletions(-) create mode 100644 api-project-component-v0/openapi/api-project-component-v0-internal.yaml create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentDeletionException.java 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..23ac1be --- /dev/null +++ b/api-project-component-v0/openapi/api-project-component-v0-internal.yaml @@ -0,0 +1,58 @@ +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/v0 + variables: + baseurl: + default: localhost:8080 + description: Development environment +tags: + - name: Project Components V0 Internal + description: API for managing project components with extended methods + +paths: + /projects/{projectId}/components/{componentId}: + delete: + tags: + - Project Components v0 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 + - name: componentId + in: path + required: true + description: Component id + schema: + type: string + responses: + '200': + description: Component information + +components: + securitySchemes: + basicAuth: + type: http + scheme: basic + bearerAuth: + type: http + scheme: bearer + bearerFormat: JWT + responses: + UnauthorizedError: + description: Authentication information is missing or invalid + headers: + WWW_Authenticate: + schema: + type: string 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/ProjectComponentsController.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsController.java index af47cca..8fe760a 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 @@ -2,8 +2,8 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; import org.opendevstack.apiservice.project.api.ProjectComponentsApi; +import org.opendevstack.apiservice.project.api.ProjectComponentsV0InternalApi; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; import org.opendevstack.apiservice.project.facade.ComponentsFacade; import org.opendevstack.apiservice.project.mapper.ComponentResponseMapper; @@ -19,7 +19,7 @@ @AllArgsConstructor @Slf4j @RequestMapping(ProjectComponentsController.API_BASE_PATH) -public class ProjectComponentsController implements ProjectComponentsApi { +public class ProjectComponentsController implements ProjectComponentsApi, ProjectComponentsV0InternalApi { public static final String API_BASE_PATH = "/api/pub/v0"; @@ -51,4 +51,11 @@ public ResponseEntity getProjectComponent(String projectId, String co return ResponseEntity.status(HttpStatus.OK).body(component); } + @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.OK).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 f31b069..131a553 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,6 +7,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.ComponentErrorKey; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; import org.opendevstack.apiservice.project.exception.ComponentRetrievalException; @@ -142,6 +143,17 @@ public ResponseEntity handleComponentCreationException( return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); } + @ExceptionHandler(ComponentDeletionException.class) + public ResponseEntity handleComponentDeletionException( + ComponentCreationException ex, + HttpServletRequest request) { + + log.error("Component deletion failed: {}", ex.getMessage(), ex); + //TODO Align all these + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getMessage()); + } + @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 f56451d..dd19a78 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; @@ -53,6 +54,7 @@ public Component getProjectComponent(String projectId, String componentId) { return component; } catch (MarketplaceException e) { log.error("Failed to retrieve component with id {} for project with id {}: {}", componentId, projectId, e.getMessage(), e); + //TODO we need to improve throwing the appropriate error throw new ComponentRetrievalException( String.format("Failed to retrieve component '%s' for project '%s': %s", componentId, projectId, e.getMessage()), e ); @@ -117,12 +119,14 @@ 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); } catch (MarketplaceException e) { log.error("Failed to delete component with id {} for project with id {}", componentId, projectId, e); - return false; + throw new ComponentDeletionException( + String.format("Failed to delete component for project '%s': %s", projectId, e.getMessage()), e + ); } } diff --git a/external-service-marketplace/pom.xml b/external-service-marketplace/pom.xml index d59b591..5a9769b 100644 --- a/external-service-marketplace/pom.xml +++ b/external-service-marketplace/pom.xml @@ -105,13 +105,6 @@ 2.2.21 - - com.google.code.findbugs - jsr305 - 3.0.2 - provided - - org.apache.httpcomponents httpclient @@ -193,7 +186,7 @@ generate - FILTER=operationId:triggerProvisionAction|notifyProvisioningStatusUpdate|createIncident + FILTER=operationId:triggerProvisionAction|notifyProvisioningStatusUpdate|deleteProvisioningStatus 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..1443710 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 also basic authentication", instanceName); + } + log.info("MarketplaceApiClient initialized for instance '{}'", instanceName); } 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 465564d..888a98f 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) */ @@ -37,4 +47,5 @@ public class MarketplaceInstanceConfig { private String oboScope; private String catalogItemId; + } \ No newline at end of file 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 bc284dd..1aca57b 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 @@ -29,9 +29,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 879eb7f..34a407e 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 @@ -13,12 +13,12 @@ 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.model.CatalogItem; -import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CreateIncidentAction; 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.service.MarketplaceService; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; @@ -142,24 +142,24 @@ public boolean provisionProjectComponent(String instanceName, String projectId, } @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 = getAuthenticatedClient(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 !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'", From d44279de571ff7b329f8c5ed2f5b23f533922f71 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Tue, 5 May 2026 18:01:59 +0200 Subject: [PATCH 17/25] Add tests. --- .../ProjectComponentsControllerTest.java | 24 +++++++ .../project/facade/ComponentsFacadeTest.java | 39 ++++++++--- .../service/MarketplaceServiceImplTest.java | 70 +++++++++++++++++++ 3 files changed, 122 insertions(+), 11 deletions(-) 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..81de1e8 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,28 @@ 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); } + + @Test + void delete_project_component_returns_ok_when_component_exists() throws MarketplaceException { + String projectId = "projectId"; + String componentId = "test-component-delete"; + + ResponseEntity response = projectComponentsController.deleteProjectComponent(projectId, componentId); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); + assertThat(response.getBody()).isEqualTo(null); + verify(componentsFacade).deleteProjectComponent(projectId, componentId); + } + + //TODO Enable this test once the API does throw an error +// @Test +// void delete_project_component_throws_not_found_when_component_does_not_exist() throws MarketplaceException { +// String projectId = "projectId"; +// String componentId = "test-component-delete"; +// +// assertThatThrownBy(() -> projectComponentsController.deleteProjectComponent(projectId, componentId)) +// .isInstanceOf(ComponentNotFoundException.class) +// .hasMessage("Component '" + componentId + "' not found for project 'projectId'"); +// 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 abc9c54..1419222 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 @@ -14,6 +14,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; @@ -115,23 +116,39 @@ 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, - "Conflict", - HttpHeaders.EMPTY, - new byte[0], - null + HttpStatus.CONFLICT, + "Conflict", + HttpHeaders.EMPTY, + new byte[0], + null ); when(marketplaceExternalService.provisionProjectComponent(eq("testProject"), anyList())) - .thenThrow(new MarketplaceException("This component name already exists, please choose another name.", conflict)); + .thenThrow(new MarketplaceException("This component name already exists, please choose another name.", conflict)); assertThatThrownBy(() -> componentsFacade.provisionProjectComponent("testProject", request)) - .isInstanceOf(ComponentAlreadyExistsException.class) - .hasMessage("This component name already exists, please choose another name."); + .isInstanceOf(ComponentAlreadyExistsException.class) + .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"); + } + + //TODO enable this test when the API behaves this way +// @Test +// void delete_project_component_throws_not_found_when_no_component_exists() throws MarketplaceException { +// assertThatThrownBy(() -> componentsFacade.deleteProjectComponent("testProject", "testComponent")) +// .isInstanceOf(ComponentDeletionException.class) +// .hasMessageContaining("Failed to delete component for project 'testProject'"); +// verify(marketplaceExternalService).deleteProjectComponent("testProject", "testComponent"); +// } } \ No newline at end of file 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 bf8a6aa..5c7f842 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 @@ -448,4 +448,74 @@ void testGetCatalogItem_NotFound_ReturnsNull() throws MarketplaceException { verify(clientFactory).getClient(instanceName); } + + @Test + void testDeleteProjectComponent_RestClientException() throws MarketplaceException { + // Arrange + String instanceName = "dev"; + String componentId = "test-component-id"; + MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); + + when(clientFactory.getDefaultInstanceName()).thenReturn(instanceName); + when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); + when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); + when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); + when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) + .thenThrow(new RestClientException("Connection failed")); + + // Act & Assert + assertThrows(MarketplaceException.class, () -> + marketplaceService.deleteProjectComponent(instanceName, componentId)); + + verify(clientFactory).getClient(instanceName); + verify(marketplaceApiClient).getApiClient(); + } + + @Test + void testDeleteProjectComponent_NotFound_ThrowsException() throws MarketplaceException { + // Arrange + String instanceName = "dev"; + String componentId = "test-component-id"; + MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); + HttpClientErrorException notFoundEx = HttpClientErrorException.create( + HttpStatus.NOT_FOUND, "Not Found", 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(notFoundEx); + when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); + + // Act + assertThrows(MarketplaceException.class, () -> + marketplaceService.deleteProjectComponent(instanceName, componentId)); + + // Assert + verify(clientFactory).getClient(instanceName); + } + + @Test + void testDeleteProjectComponent_ComponentExists_NoExceptionThrown() throws MarketplaceException { + // Arrange + String instanceName = "dev"; + String componentId = "test-component-id"; + MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); + HttpClientErrorException notFoundEx = HttpClientErrorException.create( + HttpStatus.NOT_FOUND, "Not Found", 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.ok(null)); + when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); + + // Act + marketplaceService.deleteProjectComponent(instanceName, componentId); + + // Assert + verify(clientFactory).getClient(instanceName); + } + } \ No newline at end of file From b9bec95ee7b2a4ce0f55e5efd03deb791b652c5c Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Wed, 6 May 2026 14:35:42 +0200 Subject: [PATCH 18/25] Update delete with latest API changes. --- .../api-project-component-v0-internal.yaml | 20 +- .../ProjectComponentsExceptionHandler.java | 4 +- .../openapi-component_provisioner-v1.0.0.yaml | 210 ++++++++++++++++-- .../service/impl/MarketplaceServiceImpl.java | 4 +- .../service/MarketplaceServiceImplTest.java | 12 +- 5 files changed, 209 insertions(+), 41 deletions(-) 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 index 23ac1be..1f14ffb 100644 --- a/api-project-component-v0/openapi/api-project-component-v0-internal.yaml +++ b/api-project-component-v0/openapi/api-project-component-v0-internal.yaml @@ -37,8 +37,16 @@ paths: schema: type: string responses: - '200': - description: Component information + "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: @@ -49,10 +57,4 @@ components: type: http scheme: bearer bearerFormat: JWT - responses: - UnauthorizedError: - description: Authentication information is missing or invalid - headers: - WWW_Authenticate: - schema: - type: string + 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 131a553..bc9e05a 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 @@ -144,14 +144,14 @@ public ResponseEntity handleComponentCreationException( } @ExceptionHandler(ComponentDeletionException.class) - public ResponseEntity handleComponentDeletionException( + public ResponseEntity handleComponentDeletionException( ComponentCreationException ex, HttpServletRequest request) { log.error("Component deletion failed: {}", ex.getMessage(), ex); //TODO Align all these - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ex.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); } @ExceptionHandler(ComponentRetrievalException.class) 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 b2d079c..1efe4b7 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 @@ -140,7 +140,7 @@ paths: - 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 @@ -162,24 +162,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. @@ -216,7 +238,7 @@ paths: schema: $ref: '#/components/schemas/ProvisioningDeleteRequest' responses: - "200": + "204": description: Project component properly deleted. "400": description: Bad request. @@ -269,6 +291,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: @@ -387,6 +454,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: @@ -412,4 +534,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/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 34a407e..dfa16d5 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 @@ -13,12 +13,12 @@ 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.model.CatalogItem; -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.openapi.model.ProvisioningStatusUpdateRequest; import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService; import org.springframework.stereotype.Service; import org.springframework.web.client.HttpClientErrorException; @@ -187,7 +187,7 @@ public void registerProjectComponent(String instanceName, String projectId, Stri apiClient.setBasePath(marketplaceClient.getConfig().getProvisionerActionsBaseUrl()); log.debug("Api client base path: {}", apiClient.getBasePath()); - NotifyProvisioningStatusUpdateRequest registerRequest = new NotifyProvisioningStatusUpdateRequest(); + ProvisioningStatusUpdateRequest registerRequest = new ProvisioningStatusUpdateRequest(); registerRequest.setComponentId(componentId); provisionResultsApi.notifyProvisioningStatusUpdate(projectId, "CREATED", registerRequest); } catch (HttpClientErrorException.Unauthorized | HttpClientErrorException.Forbidden e) { 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 5c7f842..fc4af5e 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 @@ -472,19 +472,19 @@ void testDeleteProjectComponent_RestClientException() throws MarketplaceExceptio } @Test - void testDeleteProjectComponent_NotFound_ThrowsException() throws MarketplaceException { + void testDeleteProjectComponent_Unauthorized_ThrowsException() throws MarketplaceException { // Arrange String instanceName = "dev"; String componentId = "test-component-id"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - HttpClientErrorException notFoundEx = HttpClientErrorException.create( - HttpStatus.NOT_FOUND, "Not Found", HttpHeaders.EMPTY, new byte[0], null); + 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(notFoundEx); + .thenThrow(unauthorizedException); when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); // Act @@ -501,14 +501,12 @@ void testDeleteProjectComponent_ComponentExists_NoExceptionThrown() throws Marke String instanceName = "dev"; String componentId = "test-component-id"; MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - HttpClientErrorException notFoundEx = HttpClientErrorException.create( - HttpStatus.NOT_FOUND, "Not Found", 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.ok(null)); + .thenReturn(ResponseEntity.status(HttpStatus.NO_CONTENT).build()); when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); // Act From 31139b6d128b2254dd93f157e09dab4c85510396 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Wed, 6 May 2026 15:35:42 +0200 Subject: [PATCH 19/25] Update delete with latest API changes. --- .../api-project-component-v0-internal.yaml | 6 +- .../ProjectComponentsController.java | 10 +-- .../ProjectComponentsInternalController.java | 29 +++++++++ .../ProjectComponentsExceptionHandler.java | 4 +- .../project/facade/ComponentsFacade.java | 39 ++++++++--- .../ProjectComponentsControllerTest.java | 23 ------- ...ojectComponentsInternalControllerTest.java | 65 +++++++++++++++++++ 7 files changed, 131 insertions(+), 45 deletions(-) create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/ProjectComponentsInternalController.java create mode 100644 api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsInternalControllerTest.java 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 index 1f14ffb..4797699 100644 --- a/api-project-component-v0/openapi/api-project-component-v0-internal.yaml +++ b/api-project-component-v0/openapi/api-project-component-v0-internal.yaml @@ -6,20 +6,20 @@ info: name: ODS Team version: v0.0.1 servers: - - url: http://{baseurl}/api/pub/v0 + - url: http://{baseurl}/api/pub/v1 variables: baseurl: default: localhost:8080 description: Development environment tags: - - name: Project Components V0 Internal + - name: Project Components Internal description: API for managing project components with extended methods paths: /projects/{projectId}/components/{componentId}: delete: tags: - - Project Components v0 Internal + - Project Components Internal summary: Delete component information operationId: deleteProjectComponent description: Deletes a specific component 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 8fe760a..d217da6 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 @@ -3,7 +3,6 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.opendevstack.apiservice.project.api.ProjectComponentsApi; -import org.opendevstack.apiservice.project.api.ProjectComponentsV0InternalApi; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; import org.opendevstack.apiservice.project.facade.ComponentsFacade; import org.opendevstack.apiservice.project.mapper.ComponentResponseMapper; @@ -19,7 +18,7 @@ @AllArgsConstructor @Slf4j @RequestMapping(ProjectComponentsController.API_BASE_PATH) -public class ProjectComponentsController implements ProjectComponentsApi, ProjectComponentsV0InternalApi { +public class ProjectComponentsController implements ProjectComponentsApi { public static final String API_BASE_PATH = "/api/pub/v0"; @@ -51,11 +50,6 @@ public ResponseEntity getProjectComponent(String projectId, String co return ResponseEntity.status(HttpStatus.OK).body(component); } - @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.OK).build(); - } + } 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 bc9e05a..86f026d 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 @@ -4,6 +4,7 @@ import lombok.extern.slf4j.Slf4j; import org.opendevstack.apiservice.project.controller.ComponentsResponseFactory; import org.opendevstack.apiservice.project.controller.ProjectComponentsController; +import org.opendevstack.apiservice.project.controller.ProjectComponentsInternalController; import org.opendevstack.apiservice.project.exception.ComponentAlreadyExistsException; import org.opendevstack.apiservice.project.exception.ComponentBadRequestException; import org.opendevstack.apiservice.project.exception.ComponentCreationException; @@ -25,7 +26,8 @@ import java.util.Map; -@RestControllerAdvice(assignableTypes = ProjectComponentsController.class) +@RestControllerAdvice(assignableTypes = {ProjectComponentsController.class, + ProjectComponentsInternalController.class}) @Slf4j public class ProjectComponentsExceptionHandler { 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 dd19a78..b2ac20f 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 @@ -119,16 +119,35 @@ private String extractHttpErrorMessage(Throwable throwable) { return throwable.getMessage(); } - public void deleteProjectComponent(String projectId, String componentId) { - try { - marketplaceExternalService.deleteProjectComponent(projectId, componentId); - } catch (MarketplaceException e) { - log.error("Failed to delete component with id {} for project with id {}", componentId, projectId, e); - throw new ComponentDeletionException( - String.format("Failed to delete component for project '%s': %s", projectId, e.getMessage()), e - ); - } - } + public void deleteProjectComponent(String projectId, String componentId) { + try { + marketplaceExternalService.deleteProjectComponent(projectId, componentId); + log.info("Successfully deleted component '{}' for project '{}'", componentId, projectId); + } catch (MarketplaceException e) { + 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) { try { 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 81de1e8..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 @@ -108,27 +108,4 @@ void get_project_component_throws_not_found_when_component_does_not_exist() thro verify(componentsFacade).getProjectComponent(projectId, componentId); } - @Test - void delete_project_component_returns_ok_when_component_exists() throws MarketplaceException { - String projectId = "projectId"; - String componentId = "test-component-delete"; - - ResponseEntity response = projectComponentsController.deleteProjectComponent(projectId, componentId); - - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isEqualTo(null); - verify(componentsFacade).deleteProjectComponent(projectId, componentId); - } - - //TODO Enable this test once the API does throw an error -// @Test -// void delete_project_component_throws_not_found_when_component_does_not_exist() throws MarketplaceException { -// String projectId = "projectId"; -// String componentId = "test-component-delete"; -// -// assertThatThrownBy(() -> projectComponentsController.deleteProjectComponent(projectId, componentId)) -// .isInstanceOf(ComponentNotFoundException.class) -// .hasMessage("Component '" + componentId + "' not found for project 'projectId'"); -// 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/controller/ProjectComponentsInternalControllerTest.java b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsInternalControllerTest.java new file mode 100644 index 0000000..82f0bef --- /dev/null +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/ProjectComponentsInternalControllerTest.java @@ -0,0 +1,65 @@ +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.externalservice.marketplace.exception.MarketplaceException; +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() throws MarketplaceException { + String projectId = "projectId"; + String componentId = "test-component-delete"; + + ResponseEntity response = projectComponentsInternalController.deleteProjectComponent(projectId, componentId); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + assertThat(response.getBody()).isEqualTo(null); + verify(componentsFacade).deleteProjectComponent(projectId, componentId); + } + + @Test + void delete_project_component_throws_exception_when_component_delete_api_throws_exception() throws MarketplaceException { + 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 From ad2e7fa35c32c9e37ed5bdc5dd39ab7a3134c02d Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Wed, 6 May 2026 16:39:01 +0200 Subject: [PATCH 20/25] Fixes after the merge with develop. --- .../project/facade/ComponentsFacade.java | 2 - .../client/MarketplaceApiClientFactory.java | 7 +- .../service/impl/MarketplaceServiceImpl.java | 5 +- .../service/MarketplaceServiceImplTest.java | 323 ++++++------------ 4 files changed, 103 insertions(+), 234 deletions(-) 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 afca11b..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 @@ -13,7 +13,6 @@ 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.ComponentDeletionException; import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; import org.opendevstack.apiservice.project.exception.ComponentRetrievalException; import org.opendevstack.apiservice.project.mapper.MarketplaceMapper; @@ -59,7 +58,6 @@ public Component getProjectComponent(String projectId, String componentId) { String.format("Failed to retrieve component '%s' for project '%s': %s", componentId, projectId, e.getMessage()), e ); } - return marketplaceMapper.mapMarketplaceComponentToV0Component(marketplaceComponent); } public void provisionProjectComponent(String projectId, CreateComponentRequest createComponentRequest) { 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 6fcc324..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. * @@ -180,6 +180,7 @@ private RestTemplate createRestTemplate(MarketplaceInstanceConfig config) { private SimpleClientHttpRequestFactory createTrustAllRequestFactory() { final javax.net.ssl.SSLSocketFactory socketFactory; try { + TrustManager[] trustAllCertificates = new TrustManager[]{ new X509TrustManager() { public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; @@ -211,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/service/impl/MarketplaceServiceImpl.java b/external-service-marketplace/src/main/java/org/opendevstack/apiservice/externalservice/marketplace/service/impl/MarketplaceServiceImpl.java index 40f8926..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; @@ -24,8 +23,6 @@ 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.openapi.model.ProvisioningStatusUpdateRequest; -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; @@ -225,7 +222,7 @@ public void registerProjectComponent(String instanceName, String projectId, Stri apiClient.setBasePath(marketplaceClient.getConfig().getProvisionerActionsBaseUrl()); log.debug("Api client base path: {}", apiClient.getBasePath()); - ProvisioningStatusUpdateRequest registerRequest = new ProvisioningStatusUpdateRequest(); + NotifyProvisioningStatusUpdateRequest registerRequest = new NotifyProvisioningStatusUpdateRequest(); registerRequest.setComponentId(componentId); provisionResultsApi.notifyProvisioningStatusUpdate(projectId, "CREATED", registerRequest); } catch (HttpClientErrorException.Unauthorized | HttpClientErrorException.Forbidden e) { 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 f9e7d66..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 @@ -7,10 +7,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.OngoingStubbing; -import org.springframework.security.core.context.SecurityContext; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.oauth2.jwt.Jwt; -import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; import org.opendevstack.apiservice.core.security.obo.OboTokenService; import org.opendevstack.apiservice.externalservice.marketplace.client.MarketplaceApiClient; import org.opendevstack.apiservice.externalservice.marketplace.client.MarketplaceApiClientFactory; @@ -20,7 +16,6 @@ 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.openapi.model.ProjectComponentInfo; import org.opendevstack.apiservice.externalservice.marketplace.service.impl.MarketplaceServiceImpl; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpHeaders; @@ -38,13 +33,11 @@ import java.nio.charset.StandardCharsets; import java.time.Instant; -import java.util.Collections; import java.util.List; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -216,79 +209,86 @@ 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; + } + }; + boolean result = service.isHealthy(); 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); } // ------------------------------------------------------------------------- @@ -953,70 +892,4 @@ void testRegisterProjectComponent_DefaultInstance() throws MarketplaceException verify(clientFactory).getClient("default"); } - @Test - void testDeleteProjectComponent_RestClientException() throws MarketplaceException { - // Arrange - String instanceName = "dev"; - String componentId = "test-component-id"; - MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - - when(clientFactory.getDefaultInstanceName()).thenReturn(instanceName); - when(clientFactory.getClient(instanceName)).thenReturn(marketplaceApiClient); - when(marketplaceApiClient.getApiClient()).thenReturn(apiClient); - when(marketplaceApiClient.getConfig()).thenReturn(instanceConfig); - when(apiClient.invokeAPI(anyString(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) - .thenThrow(new RestClientException("Connection failed")); - - // Act & Assert - assertThrows(MarketplaceException.class, () -> - marketplaceService.deleteProjectComponent(instanceName, componentId)); - - verify(clientFactory).getClient(instanceName); - verify(marketplaceApiClient).getApiClient(); - } - - @Test - void testDeleteProjectComponent_Unauthorized_ThrowsException() throws MarketplaceException { - // Arrange - String instanceName = "dev"; - String componentId = "test-component-id"; - MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - 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); - - // Act - assertThrows(MarketplaceException.class, () -> - marketplaceService.deleteProjectComponent(instanceName, componentId)); - - // Assert - verify(clientFactory).getClient(instanceName); - } - - @Test - void testDeleteProjectComponent_ComponentExists_NoExceptionThrown() throws MarketplaceException { - // Arrange - String instanceName = "dev"; - String componentId = "test-component-id"; - MarketplaceInstanceConfig instanceConfig = new MarketplaceInstanceConfig(); - - 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); - - // Act - marketplaceService.deleteProjectComponent(instanceName, componentId); - - // Assert - verify(clientFactory).getClient(instanceName); - } } \ No newline at end of file From c95e565ca5faf585be9eca4889fd8684e6bec8e2 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Wed, 6 May 2026 16:44:48 +0200 Subject: [PATCH 21/25] Fix test. --- .../project/facade/ComponentsFacadeTest.java | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) 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 5f72b38..341ffa9 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 @@ -5,7 +5,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mapstruct.factory.Mappers; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem; @@ -23,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; @@ -131,12 +131,14 @@ void delete_project_component_ends_successfully_for_existing_component() throws verify(marketplaceExternalService).deleteProjectComponent("testProject", "testComponent"); } - //TODO enable this test when the API behaves this way -// @Test -// void delete_project_component_throws_not_found_when_no_component_exists() throws MarketplaceException { -// assertThatThrownBy(() -> componentsFacade.deleteProjectComponent("testProject", "testComponent")) -// .isInstanceOf(ComponentDeletionException.class) -// .hasMessageContaining("Failed to delete component for project 'testProject'"); -// verify(marketplaceExternalService).deleteProjectComponent("testProject", "testComponent"); -// } + @Test + void delete_project_component_throws_not_found_when_no_component_exists() 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 From 84e6fbfa0493857207ecdb721b3cfa591fd72af7 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Thu, 7 May 2026 08:38:01 +0200 Subject: [PATCH 22/25] Update log message. --- .../marketplace/client/MarketplaceApiClient.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1443710..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 @@ -40,7 +40,7 @@ public MarketplaceApiClient(String instanceName, MarketplaceInstanceConfig confi if (config.getUsername() != null && config.getPassword() != null) { this.apiClient.setUsername(config.getUsername()); this.apiClient.setPassword(config.getPassword()); - log.info("MarketplaceApiClient for instance '{}' uses also basic authentication", instanceName); + log.info("MarketplaceApiClient for instance '{}' uses basic authentication", instanceName); } log.info("MarketplaceApiClient initialized for instance '{}'", instanceName); From b01b11c26b0a4857deefd8e8fae8e7dee3a17e99 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Thu, 7 May 2026 08:44:55 +0200 Subject: [PATCH 23/25] Fix code analysis findings. --- .../controller/ProjectComponentsInternalControllerTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index 82f0bef..c834a21 100644 --- 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 @@ -38,19 +38,19 @@ void tearDown() throws Exception { } @Test - void delete_project_component_returns_no_content_when_component_delete_works() throws MarketplaceException { + 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()).isEqualTo(null); + assertThat(response.getBody()).isNull(); verify(componentsFacade).deleteProjectComponent(projectId, componentId); } @Test - void delete_project_component_throws_exception_when_component_delete_api_throws_exception() throws MarketplaceException { + void delete_project_component_throws_exception_when_component_delete_api_throws_exception() { String projectId = "projectId"; String componentId = "test-component-delete"; From 0d678e68c37e739ae8ae22a4eb0ba086c5df3835 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Thu, 7 May 2026 08:58:09 +0200 Subject: [PATCH 24/25] Fix code analysis findings. --- .../controller/advice/ProjectComponentsExceptionHandler.java | 5 ----- 1 file changed, 5 deletions(-) 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 7ad8b0f..afc7ba6 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 @@ -6,15 +6,11 @@ import org.opendevstack.apiservice.project.controller.ProjectComponentsController; import org.opendevstack.apiservice.project.exception.ComponentAlreadyExistsException; import org.opendevstack.apiservice.project.exception.ComponentBadRequestException; -import org.opendevstack.apiservice.project.controller.ProjectComponentsInternalController; -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; @@ -152,7 +148,6 @@ public ResponseEntity handleComponentDeletionException( HttpServletRequest request) { log.error("Component deletion failed: {}", ex.getMessage(), ex); - //TODO Align all these return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null); } From 5dc073446f69ed30c2494a187f803a488baba6b1 Mon Sep 17 00:00:00 2001 From: "zxBCN Valeriu_Tuguran,Constantin (IT EDP) EXTERNAL" Date: Thu, 7 May 2026 11:33:41 +0200 Subject: [PATCH 25/25] Fix PR review findings. --- .../openapi/api-project-component-v0-internal.yaml | 6 ++++++ .../advice/ProjectComponentsExceptionHandler.java | 2 +- .../controller/ProjectComponentsInternalControllerTest.java | 1 - .../apiservice/project/facade/ComponentsFacadeTest.java | 2 +- 4 files changed, 8 insertions(+), 3 deletions(-) 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 index 4797699..a903d6e 100644 --- a/api-project-component-v0/openapi/api-project-component-v0-internal.yaml +++ b/api-project-component-v0/openapi/api-project-component-v0-internal.yaml @@ -30,12 +30,18 @@ paths: 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. 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 afc7ba6..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 @@ -144,7 +144,7 @@ public ResponseEntity handleComponentCreationException( @ExceptionHandler(ComponentDeletionException.class) public ResponseEntity handleComponentDeletionException( - ComponentCreationException ex, + ComponentDeletionException ex, HttpServletRequest request) { log.error("Component deletion failed: {}", ex.getMessage(), ex); 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 index c834a21..4554359 100644 --- 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 @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException; import org.opendevstack.apiservice.project.exception.ComponentDeletionException; import org.opendevstack.apiservice.project.facade.ComponentsFacade; import org.springframework.http.HttpStatus; 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 341ffa9..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 @@ -132,7 +132,7 @@ void delete_project_component_ends_successfully_for_existing_component() throws } @Test - void delete_project_component_throws_not_found_when_no_component_exists() throws MarketplaceException { + 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");