From e36dbeda26cc971a9f3f03fc802b315e8e35525b Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Mon, 13 Apr 2026 09:47:42 +0200 Subject: [PATCH 1/9] Refactor component creation and retrieval logic; enhance error handling and response mapping --- .../openapi/api-project-component-v0.yaml | 97 ++++++++-- .../project/config/JsonNullableConfig.java | 26 +++ .../controller/ComponentsResponseFactory.java | 44 ++++- .../ProjectComponentsController.java | 47 ++--- .../ProjectComponentsExceptionHandler.java | 156 ++++++++++++++++ .../exception/ComponentCreationException.java | 8 + .../project/exception/ComponentErrorKey.java | 30 ++++ .../exception/ComponentNotFoundException.java | 8 + .../project/facade/ComponentsFacade.java | 10 +- ...MarketplaceExternalServicePlaceholder.java | 27 --- .../mapper/ComponentResponseMapper.java | 3 +- .../project/mapper/MarketplaceMapper.java | 51 +++++- .../ProjectComponentsControllerTest.java | 110 +++++++----- ...ProjectComponentsExceptionHandlerTest.java | 166 ++++++++++++++++++ .../project/facade/ComponentsFacadeTest.java | 78 +++++--- .../project/util/TestObjectsBuilder.java | 40 ++--- .../marketplace/model/ProjectComponent.java | 12 +- .../impl/MarketplaceServiceMockImpl.java | 20 ++- 18 files changed, 745 insertions(+), 188 deletions(-) create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/config/JsonNullableConfig.java create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandler.java create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentCreationException.java create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentNotFoundException.java delete mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/facade/MarketplaceExternalServicePlaceholder.java create mode 100644 api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandlerTest.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 868e72e..a6bc36a 100644 --- a/api-project-component-v0/openapi/api-project-component-v0.yaml +++ b/api-project-component-v0/openapi/api-project-component-v0.yaml @@ -27,7 +27,7 @@ paths: - name: projectId in: path required: true - description: Project id + description: Project key schema: type: string requestBody: @@ -37,8 +37,8 @@ paths: schema: $ref: '#/components/schemas/CreateComponentRequest' responses: - '201': - description: Created + '200': + description: OK headers: Location: schema: @@ -49,14 +49,26 @@ paths: $ref: '#/components/schemas/CreateComponentResponse' '400': description: Bad Request + content: + application/json: + schema: + $ref: '#/components/schemas/CreateComponentResponse' '401': $ref: '#/components/responses/UnauthorizedError' '403': description: Forbidden '404': description: Endpoint not found / Project not found / Product not found + content: + application/json: + schema: + $ref: '#/components/schemas/CreateComponentResponse' '500': description: Internal Server Error + content: + application/json: + schema: + $ref: '#/components/schemas/CreateComponentResponse' /projects/{projectId}/components/{componentId}: get: @@ -69,7 +81,7 @@ paths: - name: projectId in: path required: true - description: Project id + description: Project key schema: type: string - name: componentId @@ -78,6 +90,7 @@ paths: description: Component id schema: type: string + format: uuid responses: '200': description: Component information @@ -105,6 +118,10 @@ components: schemas: Component: type: object + x-class-extra-annotation: | + @com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL) + required: + - params properties: id: type: string @@ -117,50 +134,94 @@ components: description: Description of the product productName: type: string - description: Name of the product (e.g. Docker plain) + description: Name of the product productId: type: string description: Product ID environment: - type: string - description: Environment (e.g. DEV) + $ref: '#/components/schemas/EnvironmentsTypeDTO' + description: Environment status: - type: string - description: Status of the component (e.g. READY, NOT_READY) + $ref: '#/components/schemas/EnvironmentsStatusDTO' + description: Status of the component resultTraceback: - type: string + type: object + nullable: true description: Traceback information in case of error + x-field-extra-annotation: | + @com.fasterxml.jackson.annotation.JsonInclude(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY) repositoryURL: type: string description: URL of the repository (for ODS products) params: type: object description: Additional parameters (key-value pairs) + additionalProperties: true component-type: type: string description: Type of component (ods|awx) CreateComponentResponse: type: object + required: + - timestamp + - httpStatus + - errorKey + - message + - path properties: - errorCode: + timestamp: type: integer - field: + format: int64 + httpStatus: + type: string + errorKey: + type: string + error: type: string message: type: string - location: + path: type: string - format: url CreateComponentRequest: type: object + required: + - name + - productId + - params properties: name: type: string - description: component name + description: Component name + minLength: 2 + maxLength: 52 + pattern: "^[a-z]+(-?[a-z0-9]+)*$" + x-field-extra-annotation: | + @Size(min=2, message = "The component name must contain more than 1 character.") + @Size(max=52, message = "The component name must contain less than 53 characters.") + @Pattern(regexp = "^[a-z]+(-?[a-z0-9]+)*$", message = "Only lowercase letters and numbers are allowed (example: componentname8), with optional dashes in between (example: my-component). The first character must be a letter.") productId: type: string - description: product id + description: Product id + registerOnly: + type: boolean + description: Register only flag (no provisioning) + default: false params: type: object - additionalProperties: - type: string \ No newline at end of file + description: Additional parameters (key-value pairs) + additionalProperties: true + EnvironmentsTypeDTO: + type: string + enum: + - DEV + - QA + - PROD + EnvironmentsStatusDTO: + type: string + enum: + - READY + - RUNNING + - FAILED + - DELETING + - DELETED + - UNKNOWN diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/config/JsonNullableConfig.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/config/JsonNullableConfig.java new file mode 100644 index 0000000..5230803 --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/config/JsonNullableConfig.java @@ -0,0 +1,26 @@ +package org.opendevstack.apiservice.project.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.openapitools.jackson.nullable.JsonNullable; +import org.openapitools.jackson.nullable.JsonNullableModule; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class JsonNullableConfig { + + @Bean + public Jackson2ObjectMapperBuilderCustomizer jsonNullableCustomizer() { + return builder -> { + builder.modulesToInstall(JsonNullableModule.class); + builder.postConfigurer(this::configureJsonNullableInclusion); + }; + } + + private void configureJsonNullableInclusion(ObjectMapper objectMapper) { + objectMapper.configOverride(JsonNullable.class) + .setInclude(JsonInclude.Value.construct(JsonInclude.Include.NON_ABSENT, JsonInclude.Include.NON_ABSENT)); + } +} 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..28fa4a0 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 @@ -1,24 +1,54 @@ package org.opendevstack.apiservice.project.controller; +import org.opendevstack.apiservice.project.exception.ComponentErrorKey; import org.opendevstack.apiservice.project.model.CreateComponentResponse; import org.springframework.http.HttpStatus; -public class ComponentsResponseFactory { +public final class ComponentsResponseFactory { private ComponentsResponseFactory() { } - public static CreateComponentResponse error(String projectId) { + public static CreateComponentResponse entityCreated(String projectId, String componentId) { + String path = String.format("/api/pub/v0/projects/%s/components/%s", projectId, componentId); + return ok(path, "Component created"); + } + + public static CreateComponentResponse badRequest(String path, String message, ComponentErrorKey errorKey) { + return buildResponse(HttpStatus.BAD_REQUEST, errorKey, path, message); + } + + public static CreateComponentResponse forbidden(String path, String message, ComponentErrorKey errorKey) { + return buildResponse(HttpStatus.FORBIDDEN, errorKey, path, message); + } + + public static CreateComponentResponse notFound(String path, String message, ComponentErrorKey errorKey) { + return buildResponse(HttpStatus.NOT_FOUND, errorKey, path, message); + } + + public static CreateComponentResponse internalError(String path, String message, ComponentErrorKey errorKey) { + return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, errorKey, path, message); + } + + private static CreateComponentResponse buildResponse(HttpStatus httpStatus, ComponentErrorKey errorKey, String path, + String message) { CreateComponentResponse response = new CreateComponentResponse(); - response.setErrorCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); - response.setMessage("Failed to create component for project '" + projectId + "'"); + response.setTimestamp(System.currentTimeMillis()); + response.setHttpStatus(httpStatus.name()); + response.setErrorKey(errorKey.getKey()); + response.setError(errorKey.getMessage()); + response.setMessage(message); + response.setPath(path); return response; } - public static CreateComponentResponse entityCreated(String projectId, String componentId) { + public static CreateComponentResponse ok(String path, String message) { CreateComponentResponse response = new CreateComponentResponse(); - response.setErrorCode(HttpStatus.CREATED.value()); - response.setMessage(componentId + " component created successfully in project " + projectId); + response.setTimestamp(System.currentTimeMillis()); + response.setHttpStatus(HttpStatus.OK.name()); + response.setErrorKey(ComponentErrorKey.OK.getKey()); + response.setMessage(message); + response.setPath(path); 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..d288cae 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,6 +3,8 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; 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; import org.opendevstack.apiservice.project.model.Component; @@ -13,44 +15,43 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import java.util.UUID; + @RestController @AllArgsConstructor @Slf4j -@RequestMapping("/api/pub/v0") +@RequestMapping(ProjectComponentsController.API_BASE_PATH) public class ProjectComponentsController implements ProjectComponentsApi { + public static final String API_BASE_PATH = "/api/pub/v0"; + private final ComponentsFacade componentsFacade; private final ComponentResponseMapper componentResponseMapper; @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) { - log.error("Failed to create component for project '{}'", projectId); - return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.error(projectId)); - } - return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.entityCreated(projectId, component.getId())); - } catch (Exception e) { - log.error("Error while trying to create component for project '" + projectId + "': " + e.getMessage(), e); - return componentResponseMapper.toResponseEntity(ComponentsResponseFactory.error(projectId)); + 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()) + ); } @Override - public ResponseEntity getProjectComponent(String projectId, String componentId) { - try { - Component component = componentsFacade.getProjectComponent(projectId, componentId); - log.info("Retrieved component '{}' for project '{}': {}", componentId, projectId, component); - if (component == null) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); - } - return ResponseEntity.status(HttpStatus.OK).body(component); - } catch (Exception e) { - log.error("Error retrieving component '{}' for project '{}': {}", componentId, projectId, e.getMessage(), e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + 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("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 new file mode 100644 index 0000000..f561d3d --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandler.java @@ -0,0 +1,156 @@ +package org.opendevstack.apiservice.project.controller.advice; + +import jakarta.servlet.http.HttpServletRequest; +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.ComponentCreationException; +import org.opendevstack.apiservice.project.exception.ComponentErrorKey; +import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; +import org.opendevstack.apiservice.project.model.CreateComponentResponse; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +import java.util.Map; + +@RestControllerAdvice(assignableTypes = ProjectComponentsController.class) +@Slf4j +public class ProjectComponentsExceptionHandler { + + private static final Map FIELD_ERROR_MAP = Map.of( + "name", ComponentErrorKey.COMPONENT_PARAM_NOT_MEET_REGEX, + "productId", ComponentErrorKey.INVALID_PARAMETERS, + "params", ComponentErrorKey.INVALID_PARAMETERS + ); + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException( + MethodArgumentNotValidException ex, + HttpServletRequest request) { + + log.warn("Request body validation error: {}", ex.getMessage()); + + FieldError fieldError = ex.getBindingResult().getFieldErrors().stream() + .findFirst() + .orElse(null); + + ComponentErrorKey errorKey = ComponentErrorKey.INVALID_PARAMETERS; + String message = ComponentErrorKey.INVALID_PARAMETERS.getMessage(); + + if (fieldError != null) { + errorKey = FIELD_ERROR_MAP.getOrDefault(fieldError.getField(), ComponentErrorKey.INVALID_PARAMETERS); + message = String.format("Field: %s %s", fieldError.getField(), fieldError.getDefaultMessage()); + } + + CreateComponentResponse response = ComponentsResponseFactory.badRequest( + request.getRequestURI(), + message, + errorKey + ); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } + + @ExceptionHandler(MethodArgumentTypeMismatchException.class) + public ResponseEntity handleMethodArgumentTypeMismatchException( + MethodArgumentTypeMismatchException ex, + HttpServletRequest request) { + + log.warn("Request parameter validation error: {}", ex.getMessage()); + + CreateComponentResponse response = ComponentsResponseFactory.badRequest( + request.getRequestURI(), + ex.getMessage(), + ComponentErrorKey.INVALID_PARAMETERS + ); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } + + @ExceptionHandler(ComponentNotFoundException.class) + public ResponseEntity handleComponentNotFoundException( + ComponentNotFoundException ex, + HttpServletRequest request) { + + log.warn("Component not found: {}", ex.getMessage()); + + CreateComponentResponse response = ComponentsResponseFactory.notFound( + request.getRequestURI(), + ex.getMessage(), + ComponentErrorKey.COMPONENT_NOT_FOUND + ); + + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); + } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDeniedException( + AccessDeniedException ex, + HttpServletRequest request) { + + log.warn("Access denied: {}", ex.getMessage()); + + CreateComponentResponse response = ComponentsResponseFactory.forbidden( + request.getRequestURI(), + ex.getMessage(), + ComponentErrorKey.ACCESS_DENIED + ); + + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + public ResponseEntity handleHttpMessageNotReadableException( + HttpMessageNotReadableException ex, + HttpServletRequest request) { + + log.warn("Request body format error: {}", ex.getMessage()); + + CreateComponentResponse response = ComponentsResponseFactory.badRequest( + request.getRequestURI(), + "Params property should be a valid json.", + ComponentErrorKey.COMPONENT_PARAM_INVALID_FORMAT + ); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); + } + + @ExceptionHandler(ComponentCreationException.class) + public ResponseEntity handleComponentCreationException( + ComponentCreationException ex, + HttpServletRequest request) { + + log.error("Component creation 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(Exception.class) + public ResponseEntity handleGenericException( + Exception ex, + HttpServletRequest request) { + + log.error("Unexpected error: {}", ex.getMessage(), ex); + + CreateComponentResponse response = ComponentsResponseFactory.internalError( + request.getRequestURI(), + "An error occurred while processing the request.", + ComponentErrorKey.INTERNAL_ERROR + ); + + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } +} 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 new file mode 100644 index 0000000..f035918 --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentCreationException.java @@ -0,0 +1,8 @@ +package org.opendevstack.apiservice.project.exception; + +public class ComponentCreationException extends RuntimeException { + + public ComponentCreationException(String message) { + super(message); + } +} diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java new file mode 100644 index 0000000..6d91201 --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java @@ -0,0 +1,30 @@ +package org.opendevstack.apiservice.project.exception; + +public enum ComponentErrorKey { + + OK("000", "Success"), + ACCESS_DENIED("002", "Forbidden"), + INTERNAL_ERROR("003", "Internal error"), + INVALID_PARAMETERS("006", "Bad Request"), + COMPONENT_PARAM_NOT_MEET_REGEX("010", "Bad Request"), + PROJECT_NOT_FOUND("012", "Not Found"), + COMPONENT_NOT_FOUND("013", "Not Found"), + BAD_REQUEST_BODY("014", "Bad Request"), + COMPONENT_PARAM_INVALID_FORMAT("017", "Bad Request"); + + private final String key; + private final String message; + + ComponentErrorKey(String key, String message) { + this.key = key; + this.message = message; + } + + public String getKey() { + return key; + } + + public String getMessage() { + return message; + } +} 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 new file mode 100644 index 0000000..8ee5ffc --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentNotFoundException.java @@ -0,0 +1,8 @@ +package org.opendevstack.apiservice.project.exception; + +public class ComponentNotFoundException extends RuntimeException { + + public ComponentNotFoundException(String message) { + super(message); + } +} 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..42bab0e 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 @@ -5,6 +5,8 @@ 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.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; @@ -25,7 +27,9 @@ public Component getProjectComponent(String projectId, String componentId) { ProjectComponent marketplaceComponent = marketplaceExternalService.getProjectComponent(projectId, componentId); if (marketplaceComponent == null) { log.info("Marketplace component with id {} not found", componentId); - return null; + throw new ComponentNotFoundException( + String.format("Component '%s' not found for project '%s'", componentId, projectId) + ); } return marketplaceMapper.mapMarketplaceComponentToV0Component(marketplaceComponent); } @@ -35,7 +39,9 @@ public Component createProjectComponent(String projectId, CreateComponentRequest ProjectComponent marketplaceComponent = marketplaceExternalService.createProjectComponent(projectId, createComponentParameterList); if (marketplaceComponent == null) { log.error("Failed to create component in marketplace for project with id {}", projectId); - return null; + throw new ComponentCreationException( + String.format("Failed to create component for project '%s'", projectId) + ); } return marketplaceMapper.mapMarketplaceComponentToV0Component(marketplaceComponent); } 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/ComponentResponseMapper.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/ComponentResponseMapper.java index 4c3198b..846f299 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/ComponentResponseMapper.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/mapper/ComponentResponseMapper.java @@ -9,7 +9,6 @@ public interface ComponentResponseMapper { default ResponseEntity toResponseEntity(CreateComponentResponse response) { - return new ResponseEntity<>(response, HttpStatus.valueOf(response.getErrorCode())); + return new ResponseEntity<>(response, HttpStatus.valueOf(response.getHttpStatus())); } - } 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..9101676 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 org.mapstruct.Mapper; import org.opendevstack.apiservice.externalservice.marketplace.model.CreateComponentParameter; import org.opendevstack.apiservice.externalservice.marketplace.model.ProjectComponent; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.CreateComponentRequest; +import org.opendevstack.apiservice.project.model.EnvironmentsStatusDTO; + +import org.opendevstack.apiservice.project.model.EnvironmentsTypeDTO; +import java.util.Collections; import java.util.List; +import java.util.Map; @Mapper(componentModel = "spring") public interface MarketplaceMapper { @@ -16,15 +20,54 @@ default Component mapMarketplaceComponentToV0Component(ProjectComponent source) if (source == null) { return null; } + Component target = new Component(); - target.setId(source.getComponentId()); - target.setStatus(source.getStatus()); + target.setId((source.getComponentId() != null) ? source.getComponentId().toString() : null); + target.setName(source.getName()); + target.setProductDescription(source.getProductDescription()); + target.setProductName(source.getProductName()); + target.setProductId(source.getProductId()); + target.setEnvironment(toEnvironmentType(source.getEnvironment())); + target.setStatus(toEnvironmentStatus(source.getStatus())); + target.setRepositoryURL(source.getRepositoryURL()); + target.setComponentType(source.getComponentType()); + target.setParams(Collections.emptyMap()); return target; } default List mapCreateComponentRequestToCreateComponentParameterList(CreateComponentRequest createComponentRequest) { + if (createComponentRequest == null || createComponentRequest.getParams() == null) { + return List.of(); + } + return createComponentRequest.getParams().entrySet().stream() - .map(entry -> new CreateComponentParameter(entry.getKey(), "string", entry.getValue())) + .map(this::toCreateComponentParameter) .toList(); } + + private CreateComponentParameter toCreateComponentParameter(Map.Entry entry) { + return new CreateComponentParameter(entry.getKey(), "string", String.valueOf(entry.getValue())); + } + + private EnvironmentsStatusDTO toEnvironmentStatus(String sourceStatus) { + if (sourceStatus == null || sourceStatus.isBlank()) { + return null; + } + try { + return EnvironmentsStatusDTO.fromValue(sourceStatus); + } catch (IllegalArgumentException ex) { + return null; + } + } + + private EnvironmentsTypeDTO toEnvironmentType(String sourceEnv) { + if (sourceEnv == null || sourceEnv.isBlank()) { + return null; + } + try { + return EnvironmentsTypeDTO.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 a98c778..dd9b78a 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 @@ -1,26 +1,32 @@ 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.junit.jupiter.api.extension.ExtendWith; import org.mapstruct.factory.Mappers; import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.MockitoAnnotations; +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; import org.opendevstack.apiservice.project.model.Component; import org.opendevstack.apiservice.project.model.CreateComponentRequest; import org.opendevstack.apiservice.project.model.CreateComponentResponse; -import org.opendevstack.apiservice.project.facade.ComponentsFacade; 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.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.*; +import static org.opendevstack.apiservice.project.util.TestObjectsBuilder.buildTestComponent; +import static org.opendevstack.apiservice.project.util.TestObjectsBuilder.buildTestCreateComponentRequest; -@ExtendWith(MockitoExtension.class) class ProjectComponentsControllerTest { @Mock @@ -30,71 +36,93 @@ class ProjectComponentsControllerTest { private ProjectComponentsController projectComponentsController; + private AutoCloseable openMocks; + @BeforeEach void setup() { + openMocks = MockitoAnnotations.openMocks(this); projectComponentsController = new ProjectComponentsController(componentsFacade, componentResponseMapper); } + @AfterEach + void tearDown() throws Exception { + openMocks.close(); + } + @Test - void testCreateProjectComponent_whenSuccess_thenReturnOk() throws Exception { - Component testComponent = buildTestComponent(); - String testProjectId = "testProjectId"; - CreateComponentRequest testCreateComponentRequest = buildTestCreateComponentRequest(); - CreateComponentResponse testServiceResponseSuccess = buildTestCreateComponentResponseSuccess(testComponent.getId(), - testProjectId); + void create_project_component_returns_ok_when_component_is_created() { + String projectId = "testProjectId"; + CreateComponentRequest request = buildTestCreateComponentRequest(); + Component createdComponent = buildTestComponent(); + createdComponent.setId("component-123"); - when(componentsFacade.createProjectComponent(anyString(), any(CreateComponentRequest.class))) - .thenReturn(testComponent); + when(componentsFacade.createProjectComponent(eq(projectId), any(CreateComponentRequest.class))) + .thenReturn(createdComponent); - ResponseEntity response = projectComponentsController.createProjectComponent(testProjectId, - testCreateComponentRequest); + ResponseEntity response = projectComponentsController.createProjectComponent(projectId, request); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED); + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); assertThat(response.getBody()).isNotNull(); - assertThat(response.getBody()).isEqualTo(testServiceResponseSuccess); + 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).createProjectComponent(eq(projectId), eq(request)); } @Test - void testCreateProjectComponent_whenFailure_thenReturnErrorResponse() throws Exception { - CreateComponentRequest testCreateComponentRequest = buildTestCreateComponentRequest(); - String testProjectId = "testProjectId"; - CreateComponentResponse testServiceResponseFailure = buildTestCreateComponentResponseFailure(testProjectId); + void create_project_component_returns_internal_error_when_component_creation_returns_null() { + String projectId = "testProjectId"; + CreateComponentRequest request = buildTestCreateComponentRequest(); - when(componentsFacade.createProjectComponent(anyString(), any(CreateComponentRequest.class))) + when(componentsFacade.createProjectComponent(eq(projectId), any(CreateComponentRequest.class))) .thenReturn(null); - ResponseEntity response = projectComponentsController.createProjectComponent(testProjectId, - testCreateComponentRequest); + assertThatThrownBy(() -> projectComponentsController.createProjectComponent(projectId, request)) + .isInstanceOf(ComponentCreationException.class) + .hasMessage("Failed to create component for project 'testProjectId'"); + verify(componentsFacade).createProjectComponent(eq(projectId), eq(request)); + } - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); - assertThat(response.getBody()).isNotNull(); - assertThat(response.getBody()).isEqualTo(testServiceResponseFailure); + @Test + void create_project_component_propagates_exception_when_facade_throws_exception() { + String projectId = "testProjectId"; + CreateComponentRequest request = buildTestCreateComponentRequest(); + + when(componentsFacade.createProjectComponent(eq(projectId), any(CreateComponentRequest.class))) + .thenThrow(new RuntimeException("boom")); + + assertThatThrownBy(() -> projectComponentsController.createProjectComponent(projectId, request)) + .isInstanceOf(RuntimeException.class) + .hasMessage("boom"); + verify(componentsFacade).createProjectComponent(eq(projectId), eq(request)); } @Test - void testGetProjectComponent_whenSuccess_thenReturnOk() throws Exception { + void get_project_component_returns_ok_when_component_exists() { + String projectId = "projectId"; + UUID componentId = UUID.randomUUID(); Component testComponent = buildTestComponent(); - when(componentsFacade.getProjectComponent(anyString(), anyString())) - .thenReturn(testComponent); + when(componentsFacade.getProjectComponent(projectId, componentId.toString())).thenReturn(testComponent); - ResponseEntity response = projectComponentsController.getProjectComponent("projectId", - "testId"); + ResponseEntity response = projectComponentsController.getProjectComponent(projectId, componentId); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK); - assertThat(response.getBody()).isNotNull(); assertThat(response.getBody()).isEqualTo(testComponent); + verify(componentsFacade).getProjectComponent(projectId, componentId.toString()); } @Test - void testGetProjectComponent_whenFailure_thenReturnErrorResponse() throws Exception { - when(componentsFacade.getProjectComponent(anyString(), anyString())) - .thenReturn(null); + void get_project_component_throws_not_found_when_component_does_not_exist() { + String projectId = "projectId"; + UUID componentId = UUID.randomUUID(); - ResponseEntity response = projectComponentsController.getProjectComponent("projectId", - "testId"); + when(componentsFacade.getProjectComponent(projectId, componentId.toString())).thenReturn(null); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - assertThat(response.getBody()).isNull(); + assertThatThrownBy(() -> projectComponentsController.getProjectComponent(projectId, componentId)) + .isInstanceOf(ComponentNotFoundException.class) + .hasMessage("Component '" + componentId + "' not found for project 'projectId'"); + verify(componentsFacade).getProjectComponent(projectId, componentId.toString()); } } \ 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 new file mode 100644 index 0000000..fed0062 --- /dev/null +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectComponentsExceptionHandlerTest.java @@ -0,0 +1,166 @@ +package org.opendevstack.apiservice.project.controller.advice; + +import jakarta.servlet.http.HttpServletRequest; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.opendevstack.apiservice.project.exception.ComponentCreationException; +import org.opendevstack.apiservice.project.exception.ComponentNotFoundException; +import org.opendevstack.apiservice.project.model.CreateComponentResponse; +import org.springframework.core.MethodParameter; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; + +import java.lang.reflect.Method; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +class ProjectComponentsExceptionHandlerTest { + + @Mock + private HttpServletRequest request; + + private ProjectComponentsExceptionHandler handler; + + private AutoCloseable openMocks; + + @BeforeEach + void setup() { + openMocks = MockitoAnnotations.openMocks(this); + handler = new ProjectComponentsExceptionHandler(); + when(request.getRequestURI()).thenReturn("/api/pub/v0/projects/test-project/components/"); + } + + @AfterEach + void tearDown() throws Exception { + openMocks.close(); + } + + @Test + void handle_method_argument_not_valid_exception_returns_bad_request_with_regex_error_key() throws Exception { + DummyRequest dummyRequest = new DummyRequest(); + BindingResult bindingResult = new BeanPropertyBindingResult(dummyRequest, "createComponentRequest"); + bindingResult.rejectValue("name", "Pattern", "has invalid format"); + + Method method = DummyController.class.getDeclaredMethod("dummyMethod", DummyRequest.class); + MethodParameter methodParameter = new MethodParameter(method, 0); + MethodArgumentNotValidException exception = new MethodArgumentNotValidException(methodParameter, bindingResult); + + ResponseEntity response = handler.handleMethodArgumentNotValidException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getHttpStatus()).isEqualTo(HttpStatus.BAD_REQUEST.name()); + assertThat(response.getBody().getErrorKey()).isEqualTo("010"); + assertThat(response.getBody().getMessage()).isEqualTo("Field: name has invalid format"); + assertThat(response.getBody().getPath()).isEqualTo("/api/pub/v0/projects/test-project/components/"); + } + + @Test + void handle_method_argument_type_mismatch_exception_returns_bad_request() throws Exception { + Method method = DummyController.class.getDeclaredMethod("dummyMethod", DummyRequest.class); + MethodParameter methodParameter = new MethodParameter(method, 0); + MethodArgumentTypeMismatchException exception = new MethodArgumentTypeMismatchException( + "not-a-uuid", + String.class, + "componentId", + methodParameter, + new IllegalArgumentException("Invalid UUID") + ); + + ResponseEntity response = handler.handleMethodArgumentTypeMismatchException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getErrorKey()).isEqualTo("006"); + assertThat(response.getBody().getPath()).isEqualTo("/api/pub/v0/projects/test-project/components/"); + } + + @Test + void handle_component_not_found_exception_returns_not_found() { + ComponentNotFoundException exception = new ComponentNotFoundException("Component not found"); + + ResponseEntity response = handler.handleComponentNotFoundException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getErrorKey()).isEqualTo("013"); + assertThat(response.getBody().getMessage()).isEqualTo("Component not found"); + } + + @Test + void handle_access_denied_exception_returns_forbidden() { + AccessDeniedException exception = new AccessDeniedException("Forbidden"); + + ResponseEntity response = handler.handleAccessDeniedException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getErrorKey()).isEqualTo("002"); + assertThat(response.getBody().getMessage()).isEqualTo("Forbidden"); + } + + @Test + void handle_http_message_not_readable_exception_returns_bad_request() { + HttpMessageNotReadableException exception = new HttpMessageNotReadableException("Malformed JSON"); + + ResponseEntity response = handler.handleHttpMessageNotReadableException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getErrorKey()).isEqualTo("017"); + assertThat(response.getBody().getMessage()).isEqualTo("Params property should be a valid json."); + } + + @Test + void handle_component_creation_exception_returns_internal_server_error() { + ComponentCreationException exception = new ComponentCreationException("Creation failed"); + + ResponseEntity response = handler.handleComponentCreationException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getErrorKey()).isEqualTo("003"); + assertThat(response.getBody().getMessage()).isEqualTo("Creation failed"); + } + + @Test + void handle_generic_exception_returns_internal_server_error() { + RuntimeException exception = new RuntimeException("boom"); + + ResponseEntity response = handler.handleGenericException(exception, request); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); + assertThat(response.getBody()).isNotNull(); + assertThat(response.getBody().getErrorKey()).isEqualTo("003"); + assertThat(response.getBody().getMessage()).isEqualTo("An error occurred while processing the request."); + } + + private static class DummyRequest { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @SuppressWarnings("unused") + private static class DummyController { + public void dummyMethod(@ModelAttribute DummyRequest request) { + } + } +} 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..2341ba5 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,28 +1,31 @@ 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.junit.jupiter.MockitoExtension; +import org.mockito.MockitoAnnotations; import org.opendevstack.apiservice.externalservice.marketplace.model.ProjectComponent; 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; +import org.opendevstack.apiservice.project.model.EnvironmentsStatusDTO; import java.util.List; +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.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.buildTestCreateComponentRequest; import static org.opendevstack.apiservice.project.util.TestObjectsBuilder.buildTestMarketplaceComponent; -@ExtendWith(MockitoExtension.class) class ComponentsFacadeTest { private final MarketplaceMapper marketplaceMapper = Mappers.getMapper(MarketplaceMapper.class); @@ -32,54 +35,71 @@ class ComponentsFacadeTest { private ComponentsFacade componentsFacade; + private AutoCloseable openMocks; + @BeforeEach void setup() { + openMocks = MockitoAnnotations.openMocks(this); componentsFacade = new ComponentsFacade(marketplaceExternalService, marketplaceMapper); } + @AfterEach + void tearDown() throws Exception { + openMocks.close(); + } + @Test - void testGetProjectComponent_whenSuccess_thenReturnCorrectComponent() throws Exception { - ProjectComponent testComponent = buildTestMarketplaceComponent(); + void get_project_component_returns_mapped_component_when_marketplace_returns_data() { + ProjectComponent marketplaceComponent = buildTestMarketplaceComponent(); - when(marketplaceExternalService.getProjectComponent(anyString(), eq("testId"))) - .thenReturn(testComponent); + when(marketplaceExternalService.getProjectComponent(eq("testProject"), eq("testComponent"))) + .thenReturn(marketplaceComponent); - Component retrievedComponent = componentsFacade.getProjectComponent("testId", "testId"); - assertThat(retrievedComponent.getId()).isEqualTo(testComponent.getComponentId()); - assertThat(retrievedComponent.getStatus()).isEqualTo(testComponent.getStatus()); + Component retrievedComponent = componentsFacade.getProjectComponent("testProject", "testComponent"); + + assertThat(retrievedComponent).isNotNull(); + assertThat(retrievedComponent.getId()).isEqualTo(marketplaceComponent.getComponentId().toString()); + assertThat(retrievedComponent.getStatus()).isEqualTo(EnvironmentsStatusDTO.RUNNING); + verify(marketplaceExternalService).getProjectComponent("testProject", "testComponent"); } @Test - void testGetProjectComponent_whenNoComponentFound_thenReturnNull() throws Exception { - when(marketplaceExternalService.getProjectComponent(anyString(), eq("testId"))) + void get_project_component_throws_not_found_when_marketplace_returns_null() { + when(marketplaceExternalService.getProjectComponent(eq("testProject"), eq("testComponent"))) .thenReturn(null); - Component retrievedComponent = componentsFacade.getProjectComponent("testId", "testId"); - assertThat(retrievedComponent).isNull(); + assertThatThrownBy(() -> componentsFacade.getProjectComponent("testProject", "testComponent")) + .isInstanceOf(ComponentNotFoundException.class) + .hasMessage("Component 'testComponent' not found for project 'testProject'"); + verify(marketplaceExternalService).getProjectComponent("testProject", "testComponent"); } @Test - void testCreateProjectComponent_whenSuccess_thenReturnCorrectComponent() throws Exception { - ProjectComponent testComponent = buildTestMarketplaceComponent(); - CreateComponentRequest testRequest = buildTestCreateComponentRequest(); + void create_project_component_returns_mapped_component_when_marketplace_creates_component() { + ProjectComponent marketplaceComponent = buildTestMarketplaceComponent(); + CreateComponentRequest request = buildTestCreateComponentRequest(); - when(marketplaceExternalService.createProjectComponent(anyString(), any(List.class))) - .thenReturn(testComponent); + when(marketplaceExternalService.createProjectComponent(eq("testProject"), any(List.class))) + .thenReturn(marketplaceComponent); - Component retrievedComponent = componentsFacade.createProjectComponent("testId", testRequest); - assertThat(retrievedComponent.getId()).isEqualTo(testComponent.getComponentId()); - assertThat(retrievedComponent.getStatus()).isEqualTo(testComponent.getStatus()); - } + Component createdComponent = componentsFacade.createProjectComponent("testProject", request); + assertThat(createdComponent).isNotNull(); + assertThat(createdComponent.getId()).isEqualTo(marketplaceComponent.getComponentId().toString()); + assertThat(createdComponent.getStatus()).isEqualTo(EnvironmentsStatusDTO.RUNNING); + verify(marketplaceExternalService).createProjectComponent(eq("testProject"), any(List.class)); + } @Test - void testCreateProjectComponent_whenFailure_thenReturnNull() throws Exception { - CreateComponentRequest testRequest = buildTestCreateComponentRequest(); + void create_project_component_throws_creation_exception_when_marketplace_returns_null() { + CreateComponentRequest request = buildTestCreateComponentRequest(); - when(marketplaceExternalService.createProjectComponent(anyString(), any(List.class))) + when(marketplaceExternalService.createProjectComponent(eq("testProject"), any(List.class))) .thenReturn(null); - Component retrievedComponent = componentsFacade.createProjectComponent("testId", testRequest); - assertThat(retrievedComponent).isNull(); + assertThatThrownBy(() -> componentsFacade.createProjectComponent("testProject", request)) + .isInstanceOf(ComponentCreationException.class) + .hasMessage("Failed to create component for project 'testProject'"); + verify(marketplaceExternalService).createProjectComponent(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 14cc84f..1a905fd 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,14 +1,13 @@ 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.project.model.Component; import org.opendevstack.apiservice.project.model.CreateComponentRequest; -import org.opendevstack.apiservice.project.model.CreateComponentResponse; -import org.springframework.http.HttpStatus; +import org.opendevstack.apiservice.project.model.EnvironmentsStatusDTO; +import org.opendevstack.apiservice.project.model.EnvironmentsTypeDTO; -import java.util.ArrayList; -import java.util.List; +import java.util.HashMap; +import java.util.UUID; public class TestObjectsBuilder { @@ -19,14 +18,17 @@ public static Component buildTestComponent() { Component component = new Component(); component.setId("testId"); component.setName("testComponentName"); - component.environment("testEnv"); + component.setEnvironment(EnvironmentsTypeDTO.DEV); + component.setStatus(EnvironmentsStatusDTO.RUNNING); component.setComponentType("testComponentType"); + component.setParams(new HashMap<>()); return component; } public static ProjectComponent buildTestMarketplaceComponent() { ProjectComponent component = new ProjectComponent(); - component.setComponentId("testComponentId"); + component.setComponentId(UUID.randomUUID()); + component.setStatus("RUNNING"); component.setCanBeDeleted(false); component.setComponentUrl("http://test.component.url"); return component; @@ -34,29 +36,9 @@ public static ProjectComponent buildTestMarketplaceComponent() { public static CreateComponentRequest buildTestCreateComponentRequest() { CreateComponentRequest request = new CreateComponentRequest(); - request.setName("testComponentName"); + request.setName("testcomponent"); request.setProductId("testProductId"); + request.setParams(new HashMap<>()); return request; } - - public static List buildTestMarketplaceCreateComponentParameters() { - List parameters = new ArrayList<>(); - parameters.add(new CreateComponentParameter("name", "string", "testComponentName")); - parameters.add(new CreateComponentParameter("productId", "string", "testProductId")); - return parameters; - } - - public static CreateComponentResponse buildTestCreateComponentResponseSuccess(String componentId, String projectId) { - CreateComponentResponse response = new CreateComponentResponse(); - response.setErrorCode(HttpStatus.CREATED.value()); - response.setMessage(componentId + " component created successfully in project " + projectId); - return response; - } - - public static CreateComponentResponse buildTestCreateComponentResponseFailure(String projectId) { - CreateComponentResponse response = new CreateComponentResponse(); - response.setErrorCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); - response.setMessage("Failed to create component for project '" + projectId + "'"); - return response; - } } 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 index 89b640b..5ca9c00 100644 --- 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 @@ -4,13 +4,23 @@ import lombok.Data; import lombok.NoArgsConstructor; +import java.util.UUID; + @NoArgsConstructor @AllArgsConstructor @Data public class ProjectComponent { - private String componentId; + private UUID componentId; + private String name; + private String productDescription; + private String productName; + private String productId; + private String environment; private String status; + private String resultTraceback; + private String repositoryURL; + private String componentType; private boolean canBeDeleted; private String logoUrl; private String componentUrl; 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 index 2301a76..ec1007c 100644 --- 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 @@ -6,10 +6,10 @@ 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; +import java.util.UUID; @Service @Slf4j @@ -31,17 +31,27 @@ public ProjectComponent getProjectComponent(String projectId, String componentId 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.setComponentId(UUID.randomUUID()); mockComponent.setCanBeDeleted(true); mockComponent.setStatus("CREATING"); - ComposedId composedId = new ComposedId(projectId, mockComponent.getComponentId()); + mockComponent.setName(extractParam(createComponentParams, "component_id")); + mockComponent.setProductId(extractParam(createComponentParams, "component_type")); + mockComponent.setProductName("Mock Product"); + mockComponent.setProductDescription("Mock product description"); + mockComponent.setEnvironment("DEV"); + mockComponent.setComponentType("ODS"); + ComposedId composedId = new ComposedId(projectId, mockComponent.getComponentId().toString()); mockComponentsCache.put(composedId, mockComponent); return mockComponent; } - private String generateNextId() { - return "mock-component-id-" + (mockComponentsCache.size() + 1); + private String extractParam(List params, String key) { + return params.stream() + .filter(p -> key.equals(p.getName())) + .map(CreateComponentParameter::getValue) + .findFirst() + .orElse(null); } class ComposedId { From bcf02b309c137a6c7accedc0fba2d7819ca23d6c Mon Sep 17 00:00:00 2001 From: Jorge Romero Date: Thu, 16 Apr 2026 13:38:45 +0200 Subject: [PATCH 2/9] Enhance error handling in ProjectExceptionHandler and ProjectValidationException; include additional messages for validation errors --- .../advice/ProjectExceptionHandler.java | 2 +- .../project/exception/ErrorKey.java | 2 +- .../exception/ProjectValidationException.java | 5 +++++ .../validation/ProjectRequestValidator.java | 3 ++- .../advice/ProjectExceptionHandlerTest.java | 20 +++++++++++++++++++ .../impl/ProjectExistenceServiceImpl.java | 5 ++++- 6 files changed, 33 insertions(+), 4 deletions(-) diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandler.java b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandler.java index 6f76d9e..e866b9b 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandler.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandler.java @@ -81,7 +81,7 @@ public ResponseEntity handleValidationException(ProjectVa response.setLocation(ProjectController.API_BASE_PATH); response.setError(HttpStatus.BAD_REQUEST.getReasonPhrase()); response.setErrorKey(errorKey.getKey()); - response.setMessage(errorKey.getMessage()); + response.setMessage(ex.getMessage()); // Needs to get the full message with additional info provided. return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); } diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java index fd7904b..b7247a6 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java @@ -13,7 +13,7 @@ public enum ErrorKey { ONLY_INVITED_PROJECT("008", ErrorMessage.FORBIDDEN), ONCE_PER_PROJECT("009", ErrorMessage.FORBIDDEN), COMPONENT_PARAM_NOT_MEET_REGEX("010", ErrorMessage.BAD_REQUEST), - INVALID_LOCATION("011", ErrorMessage.BAD_REQUEST), + INVALID_LOCATION("011", "Incorrect location. Valid locations are:"), PROJECT_NOT_FOUND("012", ErrorMessage.NOT_FOUND), COMPONENT_NOT_FOUND("013", ErrorMessage.NOT_FOUND), BAD_REQUEST_BODY("014", ErrorMessage.BAD_REQUEST), diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectValidationException.java b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectValidationException.java index 8b656e5..2694cfa 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectValidationException.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectValidationException.java @@ -11,5 +11,10 @@ public ProjectValidationException(ErrorKey errorKey) { super(errorKey.getMessage()); this.errorKey = errorKey; } + + public ProjectValidationException(ErrorKey errorKey, String additionalMessage) { + super(errorKey.getMessage() + " " + additionalMessage); + this.errorKey = errorKey; + } } diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidator.java b/api-project/src/main/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidator.java index effe572..f3ea98e 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidator.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidator.java @@ -56,7 +56,8 @@ private void validateLocation(CreateProjectRequest request) { } if (!locations.contains(location)) { - throw new ProjectValidationException(ErrorKey.INVALID_LOCATION); + throw new ProjectValidationException(ErrorKey.INVALID_LOCATION, + String.join(", ", locations)); } } } diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandlerTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandlerTest.java index 45435a0..ee439a0 100644 --- a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandlerTest.java +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandlerTest.java @@ -22,6 +22,7 @@ import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; +import java.util.List; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -96,6 +97,25 @@ void handle_validation_exception_returns_bad_request_response_for_missing_flavor assertNull(result.getBody().getErrorDescription()); } + @Test + void handle_validation_exception_preserves_additional_message_content() { + List validLocations = List.of("MADRID", "BARCELONA", "SANT_CUGAT"); + ProjectValidationException exception = new ProjectValidationException( + ErrorKey.INVALID_LOCATION, + String.join(",", validLocations) + ); + + ResponseEntity result = sut.handleValidationException(exception); + + assertEquals(HttpStatus.BAD_REQUEST, result.getStatusCode()); + assertNotNull(result.getBody()); + assertEquals("011", result.getBody().getErrorKey()); + assertEquals( + "Incorrect location. Valid locations are: MADRID,BARCELONA,SANT_CUGAT", + result.getBody().getMessage() + ); + } + @Test void handle_method_argument_not_valid_exception_returns_bad_request_response_for_request_body_validation_errors() { CreateProjectRequest target = new CreateProjectRequest(); diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectExistenceServiceImpl.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectExistenceServiceImpl.java index a448376..0214925 100644 --- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectExistenceServiceImpl.java +++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectExistenceServiceImpl.java @@ -12,9 +12,12 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; +import lombok.extern.slf4j.Slf4j; + import java.util.Comparator; import java.util.Set; +@Slf4j @Service public class ProjectExistenceServiceImpl implements ProjectExistenceService { @@ -31,7 +34,6 @@ public ProjectExistenceServiceImpl(BitbucketService bitbucketService, JiraServic this.openshiftService = openshiftService; this.projectService = projectService; } - @Override public boolean isProjectFound(String projectKey) throws ProjectExistenceServiceException { try { @@ -46,6 +48,7 @@ public boolean isProjectFound(String projectKey) throws ProjectExistenceServiceE } catch (OpenshiftException e) { throw new ProjectExistenceServiceException("Failed to check project in Openshift", e); } catch (RuntimeException e) { + log.error("Unexpected error while checking project existence for key '{}'", projectKey, e); throw new ProjectExistenceServiceException("Unexpected error while checking project existence", e); } } From 0feb6b8f2e8c1ac1e8924eb922077f91712ea1e4 Mon Sep 17 00:00:00 2001 From: Jorge Romero Date: Thu, 16 Apr 2026 13:50:20 +0200 Subject: [PATCH 3/9] Improve flavor request validation in FlavorRestrictionEvaluator to check for empty strings --- .../authorization/evaluator/FlavorRestrictionEvaluator.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core-security/src/main/java/org/opendevstack/apiservice/core/security/authorization/evaluator/FlavorRestrictionEvaluator.java b/core-security/src/main/java/org/opendevstack/apiservice/core/security/authorization/evaluator/FlavorRestrictionEvaluator.java index 9da0b9a..9fd93c7 100644 --- a/core-security/src/main/java/org/opendevstack/apiservice/core/security/authorization/evaluator/FlavorRestrictionEvaluator.java +++ b/core-security/src/main/java/org/opendevstack/apiservice/core/security/authorization/evaluator/FlavorRestrictionEvaluator.java @@ -37,7 +37,7 @@ public AuthorizationDecision evaluate(PolicyContext context) { } String requestedFlavor = (String) body.get("projectFlavor"); - if (requestedFlavor == null) { + if (requestedFlavor == null || requestedFlavor.trim().isEmpty()) { return AuthorizationDecision.ABSTAIN; } From a39a07a6c922cf7452089ed159ea7aabd7a7a0c1 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Mon, 20 Apr 2026 09:55:44 +0200 Subject: [PATCH 4/9] Refactor error messages in ComponentErrorKey; streamline error handling methods --- .../project/exception/ComponentErrorKey.java | 20 +++++++++++++------ .../ProjectComponentsControllerTest.java | 6 +++--- ...ProjectComponentsExceptionHandlerTest.java | 1 + .../project/facade/ComponentsFacadeTest.java | 4 ++-- 4 files changed, 20 insertions(+), 11 deletions(-) diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java index 6d91201..47f7034 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java @@ -5,12 +5,12 @@ public enum ComponentErrorKey { OK("000", "Success"), ACCESS_DENIED("002", "Forbidden"), INTERNAL_ERROR("003", "Internal error"), - INVALID_PARAMETERS("006", "Bad Request"), - COMPONENT_PARAM_NOT_MEET_REGEX("010", "Bad Request"), - PROJECT_NOT_FOUND("012", "Not Found"), - COMPONENT_NOT_FOUND("013", "Not Found"), - BAD_REQUEST_BODY("014", "Bad Request"), - COMPONENT_PARAM_INVALID_FORMAT("017", "Bad Request"); + INVALID_PARAMETERS("006", badRequest()), + COMPONENT_PARAM_NOT_MEET_REGEX("010", badRequest()), + PROJECT_NOT_FOUND("012", notFound()), + COMPONENT_NOT_FOUND("013", notFound()), + BAD_REQUEST_BODY("014", badRequest()), + COMPONENT_PARAM_INVALID_FORMAT("017", badRequest()); private final String key; private final String message; @@ -27,4 +27,12 @@ public String getKey() { public String getMessage() { return message; } + + private static String badRequest() { + return "Bad Request"; + } + + private static String notFound() { + return "Not Found"; + } } 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 dd9b78a..0b67225 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 @@ -67,7 +67,7 @@ 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(eq(projectId), eq(request)); + verify(componentsFacade).createProjectComponent(projectId, request); } @Test @@ -81,7 +81,7 @@ void create_project_component_returns_internal_error_when_component_creation_ret assertThatThrownBy(() -> projectComponentsController.createProjectComponent(projectId, request)) .isInstanceOf(ComponentCreationException.class) .hasMessage("Failed to create component for project 'testProjectId'"); - verify(componentsFacade).createProjectComponent(eq(projectId), eq(request)); + verify(componentsFacade).createProjectComponent(projectId, request); } @Test @@ -95,7 +95,7 @@ void create_project_component_propagates_exception_when_facade_throws_exception( assertThatThrownBy(() -> projectComponentsController.createProjectComponent(projectId, request)) .isInstanceOf(RuntimeException.class) .hasMessage("boom"); - verify(componentsFacade).createProjectComponent(eq(projectId), eq(request)); + verify(componentsFacade).createProjectComponent(projectId, request); } @Test 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 fed0062..e6bced2 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 @@ -161,6 +161,7 @@ public void setName(String name) { @SuppressWarnings("unused") private static class DummyController { public void dummyMethod(@ModelAttribute DummyRequest request) { + // no implementation needed for testing purposes } } } 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 2341ba5..25aab7c 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 @@ -52,7 +52,7 @@ void tearDown() throws Exception { void get_project_component_returns_mapped_component_when_marketplace_returns_data() { ProjectComponent marketplaceComponent = buildTestMarketplaceComponent(); - when(marketplaceExternalService.getProjectComponent(eq("testProject"), eq("testComponent"))) + when(marketplaceExternalService.getProjectComponent("testProject", "testComponent")) .thenReturn(marketplaceComponent); Component retrievedComponent = componentsFacade.getProjectComponent("testProject", "testComponent"); @@ -65,7 +65,7 @@ void get_project_component_returns_mapped_component_when_marketplace_returns_dat @Test void get_project_component_throws_not_found_when_marketplace_returns_null() { - when(marketplaceExternalService.getProjectComponent(eq("testProject"), eq("testComponent"))) + when(marketplaceExternalService.getProjectComponent("testProject", "testComponent")) .thenReturn(null); assertThatThrownBy(() -> componentsFacade.getProjectComponent("testProject", "testComponent")) From 788f042a3a2f1b34341c0aeb1e918149779eeea2 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Mon, 20 Apr 2026 10:23:52 +0200 Subject: [PATCH 5/9] Fix formatting of error message in ProjectExceptionHandlerTest for better readability --- .../project/controller/advice/ProjectExceptionHandlerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandlerTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandlerTest.java index ee439a0..83942c6 100644 --- a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandlerTest.java +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandlerTest.java @@ -111,7 +111,7 @@ void handle_validation_exception_preserves_additional_message_content() { assertNotNull(result.getBody()); assertEquals("011", result.getBody().getErrorKey()); assertEquals( - "Incorrect location. Valid locations are: MADRID,BARCELONA,SANT_CUGAT", + "Incorrect location. Valid locations are: MADRID,BARCELONA,SANT_CUGAT", result.getBody().getMessage() ); } From c6599fc282b221a78f918309889569b1290d2f8c Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Mon, 20 Apr 2026 10:38:00 +0200 Subject: [PATCH 6/9] Refactor error messages in ComponentErrorKey; use constants for consistency --- .../apiservice/project/exception/ComponentErrorKey.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java index 47f7034..bd48ec7 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java @@ -12,6 +12,9 @@ public enum ComponentErrorKey { BAD_REQUEST_BODY("014", badRequest()), COMPONENT_PARAM_INVALID_FORMAT("017", badRequest()); + private static final String BAD_REQUEST_MESSSAGE = "Bad Request"; + private static final String NOT_FOUND_MESSAGE = "Not Found"; + private final String key; private final String message; @@ -29,10 +32,10 @@ public String getMessage() { } private static String badRequest() { - return "Bad Request"; + return BAD_REQUEST_MESSSAGE; } private static String notFound() { - return "Not Found"; + return NOT_FOUND_MESSAGE; } } From d794fd097a5c2dbfa5afd4300d088beaf1c8dd0c Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Mon, 20 Apr 2026 13:55:35 +0200 Subject: [PATCH 7/9] Refactor error handling; replace string literals with constants in ComponentErrorKey and ErrorKey --- .../project/exception/ComponentErrorKey.java | 29 ++++------ .../exception/ComponentErrorMessage.java | 10 ++++ .../project/mapper/MarketplaceMapper.java | 53 ++++++++++--------- .../project/exception/ErrorKey.java | 30 +++++------ .../project/exception/ErrorMessage.java | 15 ++++++ 5 files changed, 76 insertions(+), 61 deletions(-) create mode 100644 api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorMessage.java diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java index bd48ec7..9af565b 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorKey.java @@ -2,18 +2,15 @@ public enum ComponentErrorKey { - OK("000", "Success"), - ACCESS_DENIED("002", "Forbidden"), - INTERNAL_ERROR("003", "Internal error"), - INVALID_PARAMETERS("006", badRequest()), - COMPONENT_PARAM_NOT_MEET_REGEX("010", badRequest()), - PROJECT_NOT_FOUND("012", notFound()), - COMPONENT_NOT_FOUND("013", notFound()), - BAD_REQUEST_BODY("014", badRequest()), - COMPONENT_PARAM_INVALID_FORMAT("017", badRequest()); - - private static final String BAD_REQUEST_MESSSAGE = "Bad Request"; - private static final String NOT_FOUND_MESSAGE = "Not Found"; + OK("000", ComponentErrorMessage.SUCCESS), + ACCESS_DENIED("002", ComponentErrorMessage.FORBIDDEN), + INTERNAL_ERROR("003", ComponentErrorMessage.INTERNAL_ERROR), + INVALID_PARAMETERS("006", ComponentErrorMessage.BAD_REQUEST), + COMPONENT_PARAM_NOT_MEET_REGEX("010", ComponentErrorMessage.BAD_REQUEST), + PROJECT_NOT_FOUND("012", ComponentErrorMessage.NOT_FOUND), + COMPONENT_NOT_FOUND("013", ComponentErrorMessage.NOT_FOUND), + BAD_REQUEST_BODY("014", ComponentErrorMessage.BAD_REQUEST), + COMPONENT_PARAM_INVALID_FORMAT("017", ComponentErrorMessage.BAD_REQUEST); private final String key; private final String message; @@ -30,12 +27,4 @@ public String getKey() { public String getMessage() { return message; } - - private static String badRequest() { - return BAD_REQUEST_MESSSAGE; - } - - private static String notFound() { - return NOT_FOUND_MESSAGE; - } } diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorMessage.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorMessage.java new file mode 100644 index 0000000..a43391d --- /dev/null +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorMessage.java @@ -0,0 +1,10 @@ +package org.opendevstack.apiservice.project.exception; + +public class ComponentErrorMessage { + + public static final String BAD_REQUEST = "Bad Request"; + public static final String NOT_FOUND = "Not Found"; + public static final String FORBIDDEN = "Forbidden"; + public static final String INTERNAL_ERROR = "Internal error"; + public static final String SUCCESS = "Success"; +} 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 9101676..7187a62 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,6 +1,9 @@ package org.opendevstack.apiservice.project.mapper; +import org.mapstruct.IterableMapping; 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.project.model.Component; @@ -9,47 +12,44 @@ import org.opendevstack.apiservice.project.model.EnvironmentsTypeDTO; -import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.UUID; @Mapper(componentModel = "spring") public interface MarketplaceMapper { - default Component mapMarketplaceComponentToV0Component(ProjectComponent source) { - if (source == null) { - return null; - } - - Component target = new Component(); - target.setId((source.getComponentId() != null) ? source.getComponentId().toString() : null); - target.setName(source.getName()); - target.setProductDescription(source.getProductDescription()); - target.setProductName(source.getProductName()); - target.setProductId(source.getProductId()); - target.setEnvironment(toEnvironmentType(source.getEnvironment())); - target.setStatus(toEnvironmentStatus(source.getStatus())); - target.setRepositoryURL(source.getRepositoryURL()); - target.setComponentType(source.getComponentType()); - target.setParams(Collections.emptyMap()); - return target; - } + @Mapping(target = "id", source = "componentId", qualifiedByName = "uuidToString") + @Mapping(target = "environment", source = "environment", qualifiedByName = "toEnvironmentType") + @Mapping(target = "status", source = "status", qualifiedByName = "toEnvironmentStatus") + @Mapping(target = "params", expression = "java(java.util.Collections.emptyMap())") + @Mapping(target = "resultTraceback", ignore = true) + Component mapMarketplaceComponentToV0Component(ProjectComponent source); default List mapCreateComponentRequestToCreateComponentParameterList(CreateComponentRequest createComponentRequest) { if (createComponentRequest == null || createComponentRequest.getParams() == null) { return List.of(); } - return createComponentRequest.getParams().entrySet().stream() - .map(this::toCreateComponentParameter) - .toList(); + return mapEntriesToCreateComponentParameterList(createComponentRequest.getParams().entrySet().stream().toList()); } - private CreateComponentParameter toCreateComponentParameter(Map.Entry entry) { - return new CreateComponentParameter(entry.getKey(), "string", String.valueOf(entry.getValue())); + @IterableMapping(qualifiedByName = "toCreateComponentParameter") + 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); + + @Named("uuidToString") + default String uuidToString(UUID sourceId) { + return sourceId != null ? sourceId.toString() : null; } - private EnvironmentsStatusDTO toEnvironmentStatus(String sourceStatus) { + @Named("toEnvironmentStatus") + default EnvironmentsStatusDTO toEnvironmentStatus(String sourceStatus) { if (sourceStatus == null || sourceStatus.isBlank()) { return null; } @@ -60,7 +60,8 @@ private EnvironmentsStatusDTO toEnvironmentStatus(String sourceStatus) { } } - private EnvironmentsTypeDTO toEnvironmentType(String sourceEnv) { + @Named("toEnvironmentType") + default EnvironmentsTypeDTO toEnvironmentType(String sourceEnv) { if (sourceEnv == null || sourceEnv.isBlank()) { return null; } diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java index b7247a6..2b1718a 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java @@ -2,10 +2,10 @@ public enum ErrorKey { - OK("000", "Success"), + OK("000", ErrorMessage.SUCCESS), PRODUCT_NOT_FOUND("001", ErrorMessage.NOT_FOUND), ACCESS_DENIED("002", ErrorMessage.FORBIDDEN), - INTERNAL_ERROR("003", "Internal error"), + INTERNAL_ERROR("003", ErrorMessage.INTERNAL_ERROR), INVALID_AUTH_HEADER("004", ErrorMessage.BAD_REQUEST), MISSING_AUTH_HEADER("005", ErrorMessage.BAD_REQUEST), INVALID_PARAMETERS("006", ErrorMessage.BAD_REQUEST), @@ -13,26 +13,26 @@ public enum ErrorKey { ONLY_INVITED_PROJECT("008", ErrorMessage.FORBIDDEN), ONCE_PER_PROJECT("009", ErrorMessage.FORBIDDEN), COMPONENT_PARAM_NOT_MEET_REGEX("010", ErrorMessage.BAD_REQUEST), - INVALID_LOCATION("011", "Incorrect location. Valid locations are:"), + INVALID_LOCATION("011", ErrorMessage.INVALID_LOCATION), PROJECT_NOT_FOUND("012", ErrorMessage.NOT_FOUND), COMPONENT_NOT_FOUND("013", ErrorMessage.NOT_FOUND), BAD_REQUEST_BODY("014", ErrorMessage.BAD_REQUEST), FORBIDDEN("015", ErrorMessage.FORBIDDEN), - DUPLICATE_RECORD("016", "Record already exists"), + DUPLICATE_RECORD("016", ErrorMessage.RECORD_ALREADY_EXISTS), COMPONENT_PARAM_INVALID_FORMAT("017", ErrorMessage.BAD_REQUEST), - PROJECT_KEY_INVALID_FORMAT("018", "projectKey not met the pattern ^[A-Z] {2}[A-Z0-9] {1,8}$"), - PROJECT_NAME_INVALID_FORMAT("019", "projectName not met the pattern ^[A-Za-z0-9 ] {0,80}$"), - PROJECT_DESCRIPTION_INVALID_FORMAT("020", "projectDescription not met the pattern ^.{0,255}$"), - PROJECT_OWNER_INVALID_FORMAT("021", "projectOwner not met the pattern ^[a-z]{1,10}$"), - PROJECT_X2ACCOUNT_INVALID_FORMAT("022", "projectX2Account not met the pattern ^x2[a-zA-Z0-9]{0,13}$"), - BAD_REQUEST_FLAVOR_CONFIG_ITEM("023", "Project flavour and config item cannot be both null"), - MANDATORY_OWNER("024", "Owner must be present if the X2 account is present"), - PROJECT_ALREADY_EXISTS("025", "Project already exists"), - PROJECT_SAME_PROJECT_NAME_ALREADY_EXISTS("026", "Project with same project name already exists"), - CLIENT_APP_NOT_REGISTERED("027", "ClientApp not registered, manual registration required"), + PROJECT_KEY_INVALID_FORMAT("018", ErrorMessage.PROJECT_KEY_NOT_MET_THE_PATTERN), + PROJECT_NAME_INVALID_FORMAT("019", ErrorMessage.PROJECT_NAME_NOT_MET_THE_PATTERN), + PROJECT_DESCRIPTION_INVALID_FORMAT("020", ErrorMessage.PROJECT_DESCRIPTION_NOT_MET_THE_PATTERN), + PROJECT_OWNER_INVALID_FORMAT("021", ErrorMessage.PROJECT_OWNER_NOT_MET_THE_PATTERN), + PROJECT_X2ACCOUNT_INVALID_FORMAT("022", ErrorMessage.PROJECT_X_2_ACCOUNT_NOT_MET_THE_PATTERN), + BAD_REQUEST_FLAVOR_CONFIG_ITEM("023", ErrorMessage.PROJECT_FLAVOUR_AND_CONFIG_ITEM_CANNOT_BE_BOTH_NULL), + MANDATORY_OWNER("024", ErrorMessage.OWNER_MUST_BE_PRESENT_IF_THE_X_2_ACCOUNT_IS_PRESENT), + PROJECT_ALREADY_EXISTS("025", ErrorMessage.PROJECT_ALREADY_EXISTS), + PROJECT_SAME_PROJECT_NAME_ALREADY_EXISTS("026", ErrorMessage.PROJECT_WITH_SAME_PROJECT_NAME_ALREADY_EXISTS), + CLIENT_APP_NOT_REGISTERED("027", ErrorMessage.CLIENT_APP_NOT_REGISTERED_MANUAL_REGISTRATION_REQUIRED), INVALID_PROJECT_FLAVOR("028", ErrorMessage.BAD_REQUEST), INVALID_CONFIG_ITEM("029", ErrorMessage.BAD_REQUEST); - + private String key; private String message; diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorMessage.java b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorMessage.java index 767ab2e..5547bbd 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorMessage.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorMessage.java @@ -5,6 +5,21 @@ public class ErrorMessage { public static final String NOT_FOUND = "Not Found"; public static final String FORBIDDEN = "Forbidden"; public static final String BAD_REQUEST = "Bad Request"; + public static final String INTERNAL_ERROR = "Internal error"; + public static final String SUCCESS = "Success"; + + public static final String INVALID_LOCATION = "Incorrect location. Valid locations are:"; + public static final String RECORD_ALREADY_EXISTS = "Record already exists"; + public static final String PROJECT_KEY_NOT_MET_THE_PATTERN = "projectKey not met the pattern ^[A-Z] {2}[A-Z0-9] {1,8}$"; + public static final String PROJECT_NAME_NOT_MET_THE_PATTERN = "projectName not met the pattern ^[A-Za-z0-9 ] {0,80}$"; + public static final String PROJECT_DESCRIPTION_NOT_MET_THE_PATTERN = "projectDescription not met the pattern ^.{0,255}$"; + public static final String PROJECT_OWNER_NOT_MET_THE_PATTERN = "projectOwner not met the pattern ^[a-z]{1,10}$"; + public static final String PROJECT_X_2_ACCOUNT_NOT_MET_THE_PATTERN = "projectX2Account not met the pattern ^x2[a-zA-Z0-9]{0,13}$"; + public static final String PROJECT_FLAVOUR_AND_CONFIG_ITEM_CANNOT_BE_BOTH_NULL = "Project flavour and config item cannot be both null"; + public static final String OWNER_MUST_BE_PRESENT_IF_THE_X_2_ACCOUNT_IS_PRESENT = "Owner must be present if the X2 account is present"; + public static final String PROJECT_ALREADY_EXISTS = "Project already exists"; + public static final String PROJECT_WITH_SAME_PROJECT_NAME_ALREADY_EXISTS = "Project with same project name already exists"; + public static final String CLIENT_APP_NOT_REGISTERED_MANUAL_REGISTRATION_REQUIRED = "ClientApp not registered, manual registration required"; private ErrorMessage() { // prevent instantiation From 7d308c92a6f5de861d42312bf1a1b85c53e50d25 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Mon, 20 Apr 2026 14:47:32 +0200 Subject: [PATCH 8/9] Refactor component API schema and model; rename EnvironmentsTypeDTO to EnvironmentsDTO and EnvironmentsStatusDTO to ComponentsStatusDTO for consistency --- .../openapi/api-project-component-v0.yaml | 8 +++---- .../project/mapper/MarketplaceMapper.java | 21 +++++++++---------- .../project/facade/ComponentsFacadeTest.java | 6 +++--- .../project/util/TestObjectsBuilder.java | 8 +++---- 4 files changed, 21 insertions(+), 22 deletions(-) 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 a6bc36a..c85d72a 100644 --- a/api-project-component-v0/openapi/api-project-component-v0.yaml +++ b/api-project-component-v0/openapi/api-project-component-v0.yaml @@ -139,10 +139,10 @@ components: type: string description: Product ID environment: - $ref: '#/components/schemas/EnvironmentsTypeDTO' + $ref: '#/components/schemas/EnvironmentsDTO' description: Environment status: - $ref: '#/components/schemas/EnvironmentsStatusDTO' + $ref: '#/components/schemas/ComponentsStatusDTO' description: Status of the component resultTraceback: type: object @@ -210,13 +210,13 @@ components: type: object description: Additional parameters (key-value pairs) additionalProperties: true - EnvironmentsTypeDTO: + EnvironmentsDTO: type: string enum: - DEV - QA - PROD - EnvironmentsStatusDTO: + ComponentsStatusDTO: type: string enum: - READY 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 7187a62..8e84500 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,10 +7,9 @@ import org.opendevstack.apiservice.externalservice.marketplace.model.CreateComponentParameter; import org.opendevstack.apiservice.externalservice.marketplace.model.ProjectComponent; 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.EnvironmentsStatusDTO; - -import org.opendevstack.apiservice.project.model.EnvironmentsTypeDTO; +import org.opendevstack.apiservice.project.model.EnvironmentsDTO; import java.util.List; import java.util.Map; @@ -20,8 +19,8 @@ public interface MarketplaceMapper { @Mapping(target = "id", source = "componentId", qualifiedByName = "uuidToString") - @Mapping(target = "environment", source = "environment", qualifiedByName = "toEnvironmentType") - @Mapping(target = "status", source = "status", qualifiedByName = "toEnvironmentStatus") + @Mapping(target = "environment", source = "environment", qualifiedByName = "toEnvironment") + @Mapping(target = "status", source = "status", qualifiedByName = "toComponentStatus") @Mapping(target = "params", expression = "java(java.util.Collections.emptyMap())") @Mapping(target = "resultTraceback", ignore = true) Component mapMarketplaceComponentToV0Component(ProjectComponent source); @@ -48,25 +47,25 @@ default String uuidToString(UUID sourceId) { return sourceId != null ? sourceId.toString() : null; } - @Named("toEnvironmentStatus") - default EnvironmentsStatusDTO toEnvironmentStatus(String sourceStatus) { + @Named("toComponentStatus") + default ComponentsStatusDTO toComponentStatus(String sourceStatus) { if (sourceStatus == null || sourceStatus.isBlank()) { return null; } try { - return EnvironmentsStatusDTO.fromValue(sourceStatus); + return ComponentsStatusDTO.fromValue(sourceStatus); } catch (IllegalArgumentException ex) { return null; } } - @Named("toEnvironmentType") - default EnvironmentsTypeDTO toEnvironmentType(String sourceEnv) { + @Named("toEnvironment") + default EnvironmentsDTO toEnvironment(String sourceEnv) { if (sourceEnv == null || sourceEnv.isBlank()) { return null; } try { - return EnvironmentsTypeDTO.fromValue(sourceEnv); + return EnvironmentsDTO.fromValue(sourceEnv); } catch (IllegalArgumentException ex) { return null; } 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 25aab7c..978dae8 100644 --- a/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/facade/ComponentsFacadeTest.java +++ b/api-project-component-v0/src/test/java/org/opendevstack/apiservice/project/facade/ComponentsFacadeTest.java @@ -12,8 +12,8 @@ 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 org.opendevstack.apiservice.project.model.EnvironmentsStatusDTO; import java.util.List; @@ -59,7 +59,7 @@ void get_project_component_returns_mapped_component_when_marketplace_returns_dat assertThat(retrievedComponent).isNotNull(); assertThat(retrievedComponent.getId()).isEqualTo(marketplaceComponent.getComponentId().toString()); - assertThat(retrievedComponent.getStatus()).isEqualTo(EnvironmentsStatusDTO.RUNNING); + assertThat(retrievedComponent.getStatus()).isEqualTo(ComponentsStatusDTO.RUNNING); verify(marketplaceExternalService).getProjectComponent("testProject", "testComponent"); } @@ -86,7 +86,7 @@ void create_project_component_returns_mapped_component_when_marketplace_creates_ assertThat(createdComponent).isNotNull(); assertThat(createdComponent.getId()).isEqualTo(marketplaceComponent.getComponentId().toString()); - assertThat(createdComponent.getStatus()).isEqualTo(EnvironmentsStatusDTO.RUNNING); + assertThat(createdComponent.getStatus()).isEqualTo(ComponentsStatusDTO.RUNNING); verify(marketplaceExternalService).createProjectComponent(eq("testProject"), any(List.class)); } 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 1a905fd..0b81f7b 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 @@ -2,9 +2,9 @@ import org.opendevstack.apiservice.externalservice.marketplace.model.ProjectComponent; 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.EnvironmentsStatusDTO; -import org.opendevstack.apiservice.project.model.EnvironmentsTypeDTO; +import org.opendevstack.apiservice.project.model.EnvironmentsDTO; import java.util.HashMap; import java.util.UUID; @@ -18,8 +18,8 @@ public static Component buildTestComponent() { Component component = new Component(); component.setId("testId"); component.setName("testComponentName"); - component.setEnvironment(EnvironmentsTypeDTO.DEV); - component.setStatus(EnvironmentsStatusDTO.RUNNING); + component.setEnvironment(EnvironmentsDTO.DEV); + component.setStatus(ComponentsStatusDTO.RUNNING); component.setComponentType("testComponentType"); component.setParams(new HashMap<>()); return component; From 941aaf2275554927e6717c02b031f6cdf3c06434 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Mon, 20 Apr 2026 14:57:47 +0200 Subject: [PATCH 9/9] Add private constructor to ComponentErrorMessage to prevent instantiation --- .../apiservice/project/exception/ComponentErrorMessage.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorMessage.java b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorMessage.java index a43391d..116d439 100644 --- a/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorMessage.java +++ b/api-project-component-v0/src/main/java/org/opendevstack/apiservice/project/exception/ComponentErrorMessage.java @@ -7,4 +7,8 @@ public class ComponentErrorMessage { public static final String FORBIDDEN = "Forbidden"; public static final String INTERNAL_ERROR = "Internal error"; public static final String SUCCESS = "Success"; + + private ComponentErrorMessage() { + // prevent instantiation + } }