Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
636cc1c
Add marketplace api client.
Apr 14, 2026
27682ee
Add marketplace api client.
Apr 14, 2026
2c9e956
Remove unused var.
Apr 14, 2026
6ee3a28
Fix sonarqube complaints.
Apr 14, 2026
cf25bfb
Add delete component functionality - not finished.
Apr 21, 2026
3abe777
Merge branch 'develop' into feature/real-marketplace-components-api-u…
Apr 21, 2026
0e72bc9
Add delete component functionality - not finished.
Apr 21, 2026
4138b18
Implement component existence check and exception handling for duplic…
angelmp01 Apr 22, 2026
911780e
Add API endpoint to retrieve extended information of a project compon…
angelmp01 Apr 27, 2026
3d708a6
Update getComponent to call the new endpoint.
Apr 29, 2026
05fa84f
Implement On-Behalf-Of (OBO) token exchange functionality and enhance…
angelmp01 Apr 29, 2026
db955b7
Fix github comments.
Apr 29, 2026
05a1abf
Refactor exception handling in getProjectComponent to use ComponentNo…
angelmp01 Apr 29, 2026
3a7c372
Add ComponentBadRequestException and enhance exception handling for b…
angelmp01 Apr 29, 2026
2febcc7
Remove unused workflow, odsNamespace, and quickstarterRepository fiel…
angelmp01 Apr 29, 2026
5dff575
Fix test.
Apr 29, 2026
d899671
Merge remote-tracking branch 'origin/feature/real-marketplace-compone…
Apr 29, 2026
007c297
Merge branch 'develop' of github.com:opendevstack/ods-api-service int…
Joselee2908 May 6, 2026
b252269
feature(api-project-component-v0): updated component creation flow to…
Joselee2908 May 6, 2026
a595de2
feature(api-project-component-v0): added registration tests for contr…
Joselee2908 May 7, 2026
7074652
Merge branch 'develop' of github.com:opendevstack/ods-api-service int…
Joselee2908 May 12, 2026
513d63b
feature(external-service-marketplace): updated register service metho…
Joselee2908 May 12, 2026
ac0bf7d
Merge branch 'develop' of github.com:opendevstack/ods-api-service int…
Joselee2908 May 15, 2026
4e49697
fix(api-project-component-v0): added catalogItemSlug and parameters t…
Joselee2908 May 18, 2026
b1bfa14
minor fixes
Joselee2908 May 18, 2026
c590e40
PR fixes avoid file generation in src folder
Joselee2908 May 18, 2026
91a5bf7
Added tests to registration workflow
Joselee2908 May 18, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,15 @@ public class ProjectComponentsController implements ProjectComponentsApi {

@Override
public ResponseEntity<CreateComponentResponse> createProjectComponent(String projectId, CreateComponentRequest createComponentRequest) {
componentsFacade.provisionProjectComponent(projectId, createComponentRequest);

log.info("Created component '{}' for project '{}'", createComponentRequest.getName(), projectId);
if (Boolean.TRUE.equals(createComponentRequest.getRegisterOnly())) {
componentsFacade.registerProjectComponent(projectId, createComponentRequest);
log.info("Registered component '{}' for project '{}'", createComponentRequest.getName(), projectId);
} else {
componentsFacade.provisionProjectComponent(projectId, createComponentRequest);
log.info("Created component '{}' for project '{}'", createComponentRequest.getName(), projectId);
}

return componentResponseMapper.toResponseEntity(
ComponentsResponseFactory.entityCreated(projectId, createComponentRequest.getName())
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.opendevstack.apiservice.project.exception.ComponentDeletionException;
import org.opendevstack.apiservice.project.exception.ComponentErrorKey;
import org.opendevstack.apiservice.project.exception.ComponentNotFoundException;
import org.opendevstack.apiservice.project.exception.ComponentRegistrationException;
import org.opendevstack.apiservice.project.exception.ComponentRetrievalException;
import org.opendevstack.apiservice.project.model.CreateComponentResponse;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -142,6 +143,22 @@ public ResponseEntity<CreateComponentResponse> handleComponentCreationException(
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}

@ExceptionHandler(ComponentRegistrationException.class)
public ResponseEntity<CreateComponentResponse> handleComponentRegisterException(
ComponentRegistrationException ex,
HttpServletRequest request) {

log.error("Component registration failed: {}", ex.getMessage(), ex);

CreateComponentResponse response = ComponentsResponseFactory.internalError(
request.getRequestURI(),
ex.getMessage(),
ComponentErrorKey.INTERNAL_ERROR
);

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}

@ExceptionHandler(ComponentDeletionException.class)
public ResponseEntity<Void> handleComponentDeletionException(
ComponentDeletionException ex,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.opendevstack.apiservice.project.exception;

public class ComponentRegistrationException extends RuntimeException {

public ComponentRegistrationException(String message) {
super(message);
}

public ComponentRegistrationException(String message, Exception e) {
super(message, e);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException;
import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem;
import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo;
import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter;
import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItem;
import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProjectComponentExtendedInfo;
import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisionActionParameter;
import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisioningStatusUpdateRequestAllOfParameters;
import org.opendevstack.apiservice.externalservice.marketplace.service.CatalogItemOperations;
import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService;
import org.opendevstack.apiservice.project.exception.CatalogItemNotFoundException;
Expand All @@ -14,6 +15,7 @@
import org.opendevstack.apiservice.project.exception.ComponentCreationException;
import org.opendevstack.apiservice.project.exception.ComponentDeletionException;
import org.opendevstack.apiservice.project.exception.ComponentNotFoundException;
import org.opendevstack.apiservice.project.exception.ComponentRegistrationException;
import org.opendevstack.apiservice.project.exception.ComponentRetrievalException;
import org.opendevstack.apiservice.project.mapper.MarketplaceMapper;
import org.opendevstack.apiservice.project.model.Component;
Expand Down Expand Up @@ -175,13 +177,19 @@ private boolean isAccessDeniedCause(Throwable throwable) {
return false;
}

public boolean registerProjectComponent(String projectId, String componentId) {
public void registerProjectComponent(String projectId, CreateComponentRequest createComponentRequest) {
String componentId = createComponentRequest.getName();
String productId = createComponentRequest.getProductId();
try {
marketplaceExternalService.registerProjectComponent(projectId, componentId);
return true;
List<ProvisioningStatusUpdateRequestAllOfParameters> parameters = marketplaceMapper
.mapCreateComponentRequestToRegisterComponentParameterList(createComponentRequest);
marketplaceExternalService.registerProjectComponent(projectId, componentId, productId, parameters);
log.info("Successfully registered component '{}' for project '{}'", componentId, projectId);
} catch (MarketplaceException e) {
log.error("Failed to register component in marketplace for project with id {}", projectId, e);
return false;
log.error("Failed to register component '{}' for project '{}': {}", componentId, projectId, e.getMessage(), e);
throw new ComponentRegistrationException(
String.format("Failed to register component '%s' for project '%s': %s", componentId, projectId, e.getMessage()), e
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

import org.mapstruct.Mapper;
import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException;
import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem;
import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItemUserAction;
import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItemUserActionParameter;
import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo;
import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProvisionActionParameter;
import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItem;
import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItemUserAction;
import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItemUserActionParameter;
import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProjectComponentExtendedInfo;
import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisionActionParameter;
import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProvisioningStatusUpdateRequestAllOfParameters;
import org.opendevstack.apiservice.project.model.Component;
import org.opendevstack.apiservice.project.model.CreateComponentRequest;
import org.opendevstack.apiservice.project.model.EnvironmentsDTO;
Expand Down Expand Up @@ -67,10 +68,40 @@ default List<ProvisionActionParameter> mapCreateComponentRequestToCreateComponen
return parameters;
}

default List<ProvisioningStatusUpdateRequestAllOfParameters> mapCreateComponentRequestToRegisterComponentParameterList(
CreateComponentRequest createComponentRequest) {
if (createComponentRequest == null) {
return List.of();
}

List<ProvisioningStatusUpdateRequestAllOfParameters> parameters = new ArrayList<>();
if (createComponentRequest.getParams() != null && !createComponentRequest.getParams().isEmpty()) {
createComponentRequest.getParams().forEach((name, value) -> {
parameters.add(createRegisterParameter(name, value));
});
}

return parameters;
}

default ProvisionActionParameter createParameter(String name, Object value, String type) {
return new ProvisionActionParameter().name(name).type(type).value(value);
}

default ProvisioningStatusUpdateRequestAllOfParameters createRegisterParameter(String name, Object value) {
List<String> values;
if (value == null) {
values = List.of("");
} else if (value instanceof List<?> list) {
values = list.stream()
.map(Object::toString)
.toList();
} else {
values = List.of(value.toString());
}
return new ProvisioningStatusUpdateRequestAllOfParameters().name(name).values(values);
}

/**
* Builds a map of parameter name -> type from the catalog item user actions.
* The type comes from the {@link CatalogItemUserActionParameter#getType()} value
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,25 @@ void create_project_component_returns_ok_with_component_name_in_path() {
verify(componentsFacade).provisionProjectComponent(projectId, request);
}

@Test
void create_project_component_with_register_only_returns_ok_with_component_name_in_path() {
String projectId = "testProjectId";
CreateComponentRequest request = buildTestCreateComponentRequest(); // name = "testcomponent"
request.setRegisterOnly(Boolean.TRUE);

doNothing().when(componentsFacade).registerProjectComponent(eq(projectId), eq(request));

ResponseEntity<CreateComponentResponse> response = projectComponentsController.createProjectComponent(projectId, request);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getHttpStatus()).isEqualTo(HttpStatus.OK.name());
assertThat(response.getBody().getErrorKey()).isEqualTo("000");
assertThat(response.getBody().getMessage()).isEqualTo("Component created");
assertThat(response.getBody().getPath()).isEqualTo("/api/pub/v0/projects/testProjectId/components/testcomponent");
verify(componentsFacade).registerProjectComponent(projectId, request);
}

@Test
void create_project_component_propagates_exception_when_facade_throws_exception() {
String projectId = "testProjectId";
Expand All @@ -80,6 +99,38 @@ void create_project_component_propagates_exception_when_facade_throws_exception(
verify(componentsFacade).provisionProjectComponent(projectId, request);
}

@Test
void create_project_component_with_register_only_propagates_exception_when_facade_throws_exception() {
String projectId = "testProjectId";
CreateComponentRequest request = buildTestCreateComponentRequest();
request.setRegisterOnly(Boolean.TRUE);

org.mockito.Mockito.doThrow(new RuntimeException("boom"))
.when(componentsFacade).registerProjectComponent(eq(projectId), eq(request));

assertThatThrownBy(() -> projectComponentsController.createProjectComponent(projectId, request))
.isInstanceOf(RuntimeException.class)
.hasMessage("boom");
verify(componentsFacade).registerProjectComponent(projectId, request);
}

@Test
void create_project_component_with_register_only_false_delegates_to_provision_project_component() {
String projectId = "testProjectId";
CreateComponentRequest request = buildTestCreateComponentRequest();
request.setRegisterOnly(Boolean.FALSE);

doNothing().when(componentsFacade).provisionProjectComponent(eq(projectId), any(CreateComponentRequest.class));

ResponseEntity<CreateComponentResponse> response = projectComponentsController.createProjectComponent(projectId, request);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getMessage()).isEqualTo("Component created");
assertThat(response.getBody().getPath()).isEqualTo("/api/pub/v0/projects/testProjectId/components/testcomponent");
verify(componentsFacade).provisionProjectComponent(projectId, request);
}

@Test
void get_project_component_returns_ok_when_component_exists() throws MarketplaceException {
String projectId = "projectId";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.opendevstack.apiservice.project.exception.ComponentAlreadyExistsException;
import org.opendevstack.apiservice.project.exception.ComponentCreationException;
import org.opendevstack.apiservice.project.exception.ComponentNotFoundException;
import org.opendevstack.apiservice.project.exception.ComponentRegistrationException;
import org.opendevstack.apiservice.project.model.CreateComponentResponse;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -149,6 +150,20 @@ void handle_component_already_exists_exception_returns_conflict() {
assertThat(response.getBody().getMessage()).isEqualTo("This component name already exists, please choose another name.");
}

@Test
void handle_component_registration_exception_returns_internal_server_error() {
ComponentRegistrationException exception = new ComponentRegistrationException("Registration failed");

ResponseEntity<CreateComponentResponse> response = handler.handleComponentRegisterException(exception, request);

assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getHttpStatus()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.name());
assertThat(response.getBody().getErrorKey()).isEqualTo("003");
assertThat(response.getBody().getMessage()).isEqualTo("Registration failed");
assertThat(response.getBody().getPath()).isEqualTo("/api/pub/v0/projects/test-project/components/");
}

@Test
void handle_generic_exception_returns_internal_server_error() {
RuntimeException exception = new RuntimeException("boom");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.opendevstack.apiservice.externalservice.marketplace.exception.MarketplaceException;
import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.CatalogItem;
import org.opendevstack.apiservice.externalservice.marketplace.openapi.model.ProjectComponentExtendedInfo;
import org.opendevstack.apiservice.externalservice.marketplace.client.model.CatalogItem;
import org.opendevstack.apiservice.externalservice.marketplace.client.model.ProjectComponentExtendedInfo;
import org.opendevstack.apiservice.externalservice.marketplace.service.MarketplaceService;
import org.opendevstack.apiservice.project.exception.ComponentAlreadyExistsException;
import org.opendevstack.apiservice.project.exception.ComponentCreationException;
import org.opendevstack.apiservice.project.exception.ComponentDeletionException;
import org.opendevstack.apiservice.project.exception.ComponentNotFoundException;
import org.opendevstack.apiservice.project.exception.ComponentRegistrationException;
import org.opendevstack.apiservice.project.mapper.MarketplaceMapper;
import org.opendevstack.apiservice.project.model.Component;
import org.opendevstack.apiservice.project.model.ComponentsStatusDTO;
Expand All @@ -28,6 +29,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.opendevstack.apiservice.project.util.TestObjectsBuilder.buildTestCatalogItem;
Expand Down Expand Up @@ -103,8 +105,8 @@ void create_project_component_throws_creation_exception_when_marketplace_returns
verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), anyList());
}

@Test
void create_project_component_throws_already_exists_when_marketplace_returns_conflict() throws MarketplaceException {
@Test
void create_project_component_throws_already_exists_when_marketplace_returns_conflict() throws MarketplaceException {
CreateComponentRequest request = buildTestCreateComponentRequest();
HttpClientErrorException conflict = HttpClientErrorException.Conflict.create(
HttpStatus.CONFLICT,
Expand All @@ -121,8 +123,52 @@ void create_project_component_throws_already_exists_when_marketplace_returns_con
.isInstanceOf(ComponentAlreadyExistsException.class)
.hasMessage("This component name already exists, please choose another name.");
verify(marketplaceExternalService).provisionProjectComponent(eq("testProject"), anyList());
}
}

@Test
void register_project_component_ends_successfully_when_marketplace_registration_succeeds() throws MarketplaceException {
CreateComponentRequest request = buildTestCreateComponentRequest();
String projectId = "testProjectId";

doNothing().when(marketplaceExternalService).registerProjectComponent(eq(projectId), anyString(), anyString(), anyList());

componentsFacade.registerProjectComponent(projectId, request);

verify(marketplaceExternalService).registerProjectComponent(eq(projectId), anyString(), anyString(), anyList());
}

@Test
void register_project_component_passes_correct_component_id_and_product_id_to_marketplace() throws MarketplaceException {
CreateComponentRequest request = buildTestCreateComponentRequest();
// name = "testcomponent", productId = "testProductId"
String projectId = "testProjectId";

doNothing().when(marketplaceExternalService).registerProjectComponent(
eq(projectId), eq("testcomponent"), eq("testProductId"), anyList());

componentsFacade.registerProjectComponent(projectId, request);

verify(marketplaceExternalService).registerProjectComponent(
eq(projectId), eq("testcomponent"), eq("testProductId"), anyList());
}

@Test
void register_project_component_throws_registration_exception_wrapping_original_cause() throws MarketplaceException {
CreateComponentRequest request = buildTestCreateComponentRequest();
String projectId = "testProjectId";
MarketplaceException originalCause = new MarketplaceException("downstream error", new RuntimeException("root"));

doThrow(originalCause)
.when(marketplaceExternalService)
.registerProjectComponent(eq(projectId), anyString(), anyString(), anyList());

assertThatThrownBy(() -> componentsFacade.registerProjectComponent(projectId, request))
.isInstanceOf(ComponentRegistrationException.class)
.hasMessage("Failed to register component 'testcomponent' for project 'testProjectId': downstream error")
.hasCause(originalCause);

verify(marketplaceExternalService).registerProjectComponent(eq(projectId), anyString(), anyString(), anyList());
}

@Test
void delete_project_component_ends_successfully_for_existing_component() throws MarketplaceException {
Expand Down
Loading
Loading