From 9cbe25f7632962788e3151d510b4093ccffef9d4 Mon Sep 17 00:00:00 2001 From: Joselee2908 Date: Tue, 23 Jun 2026 10:49:05 +0200 Subject: [PATCH 1/5] Add internal API for updating project status and enhance validation --- api-project/openapi/api-project-internal.yaml | 100 ++++++++++++++++++ api-project/openapi/api-project.yaml | 2 +- api-project/pom.xml | 29 +++++ .../project/controller/ProjectController.java | 2 +- .../controller/ProjectInternalController.java | 57 ++++++++++ .../project/exception/ErrorKey.java | 3 +- .../project/exception/ErrorMessage.java | 1 + .../project/facade/ProjectsFacade.java | 3 + .../facade/impl/ProjectsFacadeImpl.java | 10 ++ .../validation/ProjectRequestValidator.java | 28 ++++- .../controller/ProjectControllerTest.java | 2 +- .../ProjectRequestValidatorTest.java | 85 +++++++++++---- application.yaml | 1 + .../service/ProjectService.java | 2 + .../service/impl/ProjectServiceImpl.java | 12 +++ 15 files changed, 314 insertions(+), 23 deletions(-) create mode 100644 api-project/openapi/api-project-internal.yaml create mode 100644 api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectInternalController.java diff --git a/api-project/openapi/api-project-internal.yaml b/api-project/openapi/api-project-internal.yaml new file mode 100644 index 00000000..f1d5d01d --- /dev/null +++ b/api-project/openapi/api-project-internal.yaml @@ -0,0 +1,100 @@ +openapi: 3.0.3 +info: + title: ODS API Server + description: API documentation for ODS (Open DevStack) API Service + contact: + name: ODS Team + version: v0.0.1 +servers: + - url: http://{baseurl}/api/pub/v0 + variables: + baseurl: + default: localhost:8080 + description: Development environment +tags: +- name: Projects Internal + description: API for manage EDP projects with extended methods +paths: + /projects/{projectKey}: + patch: + tags: + - Projects Internal + summary: Updates projects by project key. + description: Updates the current status and details of the project identified by the given project key. + operationId: updateProject + parameters: + - name: projectKey + in: path + required: true + schema: + type: string + description: Project key to retrieve information. + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateProjectRequest' + responses: + '204': + description: Project information updated successfully. + '404': + description: Project not found. + content: + application/json: + schema: + $ref: '#/components/schemas/CreateProjectResponse' + "401": + description: Invalid client token on the request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + "403": + description: Insufficient permissions for the client to access the resource. + content: + application/json: + schema: + $ref: '#/components/schemas/RestErrorMessage' + '500': + description: Internal server error. + content: + application/json: + schema: + $ref: '#/components/schemas/CreateProjectResponse' +components: + schemas: + RestErrorMessage: + properties: + message: + type: string + required: + - message + UpdateProjectRequest: + type: object + properties: + status: + type: string + description: Status of the project. If not in the allowed list, error INVALID_STATUS (030) is returned. + additionalProperties: false + CreateProjectResponse: + type: object + properties: + projectKey: + type: string + httStatus: + type: string + status: + type: string + projectFlavor: + type: string + message: + type: string + error: + type: string + errorKey: + type: string + errorDescription: + type: string + location: + type: string \ No newline at end of file diff --git a/api-project/openapi/api-project.yaml b/api-project/openapi/api-project.yaml index 9a02630d..681074b1 100644 --- a/api-project/openapi/api-project.yaml +++ b/api-project/openapi/api-project.yaml @@ -12,7 +12,7 @@ servers: default: localhost:8080 description: Development environment tags: -- name: Project +- name: Projects description: API for manage EDP projects. paths: /projects: diff --git a/api-project/pom.xml b/api-project/pom.xml index 7172641e..799043f6 100644 --- a/api-project/pom.xml +++ b/api-project/pom.xml @@ -168,6 +168,35 @@ + + generate-api-project-extended + + generate + + + spring + ${project.basedir} + spring-boot + ${project.basedir}/openapi/api-project-internal.yaml + org.opendevstack.apiservice.project.api + org.opendevstack.apiservice.project.model + org.opendevstack.apiservice.project + false + false + false + false + false + false + + true + true + springdoc + true + true + true + + + diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java index 54c33275..beea15b4 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java @@ -37,7 +37,7 @@ public class ProjectController implements ProjectsApi { @PostMapping @Override public ResponseEntity createProject(@Valid @RequestBody CreateProjectRequest createProjectRequest) { - projectRequestValidator.validate(createProjectRequest); + projectRequestValidator.validateCreateRequest(createProjectRequest); UUID clientId = JwtUtils.getClientId(); CreateProjectResponse projectResponse = projectsFacade.createProject(createProjectRequest, clientId); diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectInternalController.java b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectInternalController.java new file mode 100644 index 00000000..88691d8f --- /dev/null +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectInternalController.java @@ -0,0 +1,57 @@ +package org.opendevstack.apiservice.project.controller; + +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.core.security.jwt.JwtUtils; +import org.opendevstack.apiservice.project.api.ProjectsInternalApi; +import org.opendevstack.apiservice.project.facade.ProjectsFacade; +import org.opendevstack.apiservice.project.model.UpdateProjectRequest; +import org.opendevstack.apiservice.project.model.CreateProjectResponse; +import org.opendevstack.apiservice.project.validation.ProjectRequestValidator; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.UUID; + +@RestController +@RequestMapping(ProjectInternalController.API_BASE_PATH) +@AllArgsConstructor +@Slf4j +public class ProjectInternalController implements ProjectsInternalApi { + + public static final String API_BASE_PATH = "/api/pub/v0/projects"; + + private static final String HTTP_HEADER_LOCATION = "Location"; + + private final ProjectsFacade projectsFacade; + + private final ProjectRequestValidator projectRequestValidator; + + @PatchMapping("/{projectKey}") + @Override + public ResponseEntity updateProject(@PathVariable String projectKey, + @Valid @RequestBody UpdateProjectRequest updateProjectRequest) { + String location = API_BASE_PATH + "/" + projectKey; + projectRequestValidator.validateUpdateRequest(updateProjectRequest); + + Boolean updated = projectsFacade.updateProject(projectKey, updateProjectRequest); + + if (updated.equals(Boolean.FALSE)) { + return ResponseEntity + .status(HttpStatus.NOT_FOUND) + .header(HTTP_HEADER_LOCATION, location) + .build(); + // .body(ProjectResponseFactory.notFound(projectKey, location)); + } + return ResponseEntity + .status(HttpStatus.NO_CONTENT) + .header(HTTP_HEADER_LOCATION, location) + .build(); + } +} 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 2b1718a0..a7755a78 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 @@ -31,7 +31,8 @@ public enum ErrorKey { 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); + INVALID_CONFIG_ITEM("029", ErrorMessage.BAD_REQUEST), + INVALID_STATUS("030", ErrorMessage.INVALID_STATUS); 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 5547bbdd..8c8ed276 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 @@ -9,6 +9,7 @@ public class ErrorMessage { public static final String SUCCESS = "Success"; public static final String INVALID_LOCATION = "Incorrect location. Valid locations are:"; + public static final String INVALID_STATUS = "Incorrect status. Valid status 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}$"; diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/facade/ProjectsFacade.java b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/ProjectsFacade.java index f8b0112e..9424c223 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/facade/ProjectsFacade.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/ProjectsFacade.java @@ -2,6 +2,7 @@ import org.opendevstack.apiservice.project.model.CreateProjectRequest; import org.opendevstack.apiservice.project.model.CreateProjectResponse; +import org.opendevstack.apiservice.project.model.UpdateProjectRequest; import java.util.UUID; @@ -10,4 +11,6 @@ public interface ProjectsFacade { CreateProjectResponse createProject(CreateProjectRequest request, UUID clientId); CreateProjectResponse getProject(String projectKey); + + boolean updateProject(String projectKey, UpdateProjectRequest request); } \ No newline at end of file diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImpl.java b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImpl.java index 8e3b80d6..36fad5dc 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImpl.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImpl.java @@ -11,6 +11,7 @@ import org.opendevstack.apiservice.project.mapper.ProjectMapper; import org.opendevstack.apiservice.project.model.CreateProjectRequest; import org.opendevstack.apiservice.project.model.CreateProjectResponse; +import org.opendevstack.apiservice.project.model.UpdateProjectRequest; import org.opendevstack.apiservice.project.service.ClientAppService; import org.opendevstack.apiservice.serviceproject.model.ProjectRequest; import org.opendevstack.apiservice.serviceproject.model.ProjectResponse; @@ -84,6 +85,15 @@ public CreateProjectResponse getProject(String projectKey) { return projectMapper.toApiResponse(projectService.getProject(projectKey)); } + @Override + public boolean updateProject(String projectKey, UpdateProjectRequest request) { + ProjectResponse project = projectService.getProject(projectKey); + if (project == null) return false; + + projectService.updateProjectStatus(projectKey, request.getStatus()); + return true; + } + private ProjectResponse registerProject(ProjectCreationCommand command) { ProjectRequest projectRequest = projectMapper.toServiceRequest(command); 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 f3ea98e7..b53e64f7 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 @@ -3,9 +3,12 @@ import org.opendevstack.apiservice.project.exception.ErrorKey; import org.opendevstack.apiservice.project.exception.ProjectValidationException; import org.opendevstack.apiservice.project.model.CreateProjectRequest; +import org.opendevstack.apiservice.project.model.UpdateProjectRequest; +import org.opendevstack.apiservice.serviceproject.model.Status; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +import java.util.Arrays; import java.util.List; @Component @@ -13,12 +16,19 @@ public class ProjectRequestValidator { @Value("${apis.projects.locations}") private List locations; + + @Value("${apis.projects.status}") + private List statusList; - public void validate(CreateProjectRequest request) { + public void validateCreateRequest(CreateProjectRequest request) { validateFlavorOrConfigItem(request); validateOwnerIfX2AccountIsPresent(request); validateLocation(request); } + + public void validateUpdateRequest(UpdateProjectRequest request) { + validateStatus(request); + } private void validateFlavorOrConfigItem(CreateProjectRequest request) { String projectFlavor = request.getProjectFlavor(); @@ -60,4 +70,20 @@ private void validateLocation(CreateProjectRequest request) { String.join(", ", locations)); } } + + private void validateStatus(UpdateProjectRequest request) { + String status = request.getStatus(); + + if (status == null || status.trim().isEmpty()) { + return; + } + + List statusList = Arrays.stream(Status.values()) + .map(e -> e.getDbValue()) + .toList(); + if (!statusList.contains(status)) { + throw new ProjectValidationException(ErrorKey.INVALID_STATUS, + String.join(", ", statusList)); + } + } } diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerTest.java index d184ef9c..2e1d53f8 100644 --- a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerTest.java +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerTest.java @@ -75,7 +75,7 @@ void create_project_returns_ok_when_creation_succeeds() { assertThat(result.getBody().getError()).isNull(); assertThat(result.getBody().getErrorKey()).isEqualTo("000"); assertThat(result.getBody().getErrorDescription()).isNull(); - verify(projectRequestValidator).validate(request); + verify(projectRequestValidator).validateCreateRequest(request); verify(projectsFacade).createProject(any(CreateProjectRequest.class), any(UUID.class)); } diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidatorTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidatorTest.java index 596f39cd..f8265926 100644 --- a/api-project/src/test/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidatorTest.java +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidatorTest.java @@ -7,6 +7,7 @@ import org.opendevstack.apiservice.project.exception.ErrorKey; import org.opendevstack.apiservice.project.exception.ProjectValidationException; import org.opendevstack.apiservice.project.model.CreateProjectRequest; +import org.opendevstack.apiservice.project.model.UpdateProjectRequest; import java.lang.reflect.Field; import java.util.List; @@ -30,7 +31,7 @@ void setUp() throws NoSuchFieldException, IllegalAccessException { } @Test - void validate_throws_exception_when_project_flavor_and_config_item_both_null() { + void validateCreateRequest_throws_exception_when_project_flavor_and_config_item_both_null() { CreateProjectRequest request = new CreateProjectRequest(); request.setProjectName("Valid Name"); request.setProjectFlavor(null); @@ -38,14 +39,14 @@ void validate_throws_exception_when_project_flavor_and_config_item_both_null() { ProjectValidationException exception = assertThrows( ProjectValidationException.class, - () -> sut.validate(request) + () -> sut.validateCreateRequest(request) ); assertEquals(ErrorKey.BAD_REQUEST_FLAVOR_CONFIG_ITEM, exception.getErrorKey()); } @Test - void validate_throws_exception_when_project_flavor_and_config_item_both_empty() { + void validateCreateRequest_throws_exception_when_project_flavor_and_config_item_both_empty() { CreateProjectRequest request = new CreateProjectRequest(); request.setProjectName("Valid Name"); request.setProjectFlavor(""); @@ -53,7 +54,7 @@ void validate_throws_exception_when_project_flavor_and_config_item_both_empty() ProjectValidationException exception = assertThrows( ProjectValidationException.class, - () -> sut.validate(request) + () -> sut.validateCreateRequest(request) ); assertEquals(ErrorKey.BAD_REQUEST_FLAVOR_CONFIG_ITEM, exception.getErrorKey()); @@ -65,17 +66,17 @@ void validate_throws_exception_when_project_flavor_and_config_item_both_empty() "null, JIRA", "STANDARD, JIRA" }) - void validate_succeeds_when_flavor_or_config_item_provided(String projectFlavor, String configurationItem) { + void validateCreateRequest_succeeds_when_flavor_or_config_item_provided(String projectFlavor, String configurationItem) { CreateProjectRequest request = new CreateProjectRequest(); request.setProjectName("Valid Name"); request.setProjectFlavor("null".equals(projectFlavor) ? null : projectFlavor); request.setConfigurationItem("null".equals(configurationItem) ? null : configurationItem); - assertDoesNotThrow(() -> sut.validate(request)); + assertDoesNotThrow(() -> sut.validateCreateRequest(request)); } @Test - void validate_throws_exception_when_x2account_present_but_owner_missing() { + void validateCreateRequest_throws_exception_when_x2account_present_but_owner_missing() { CreateProjectRequest request = new CreateProjectRequest(); request.setProjectName("Valid"); request.setProjectFlavor("STANDARD"); @@ -83,7 +84,7 @@ void validate_throws_exception_when_x2account_present_but_owner_missing() { request.setOwner(null); ProjectValidationException exception = - assertThrows(ProjectValidationException.class, () -> sut.validate(request)); + assertThrows(ProjectValidationException.class, () -> sut.validateCreateRequest(request)); assertEquals(ErrorKey.MANDATORY_OWNER, exception.getErrorKey()); } @@ -95,7 +96,7 @@ void validate_throws_exception_when_x2account_present_but_owner_missing() { "null, owner1", "x2valid, ownerX" }) - void validate_succeeds_when_owner_present_or_x2account_missing(String x2, String owner) { + void validateCreateRequest_succeeds_when_owner_present_or_x2account_missing(String x2, String owner) { CreateProjectRequest request = new CreateProjectRequest(); request.setProjectName("Valid"); request.setProjectFlavor("STANDARD"); @@ -103,27 +104,27 @@ void validate_succeeds_when_owner_present_or_x2account_missing(String x2, String request.setX2OdsAccount("null".equals(x2) ? null : x2); request.setOwner(owner); - assertDoesNotThrow(() -> sut.validate(request)); + assertDoesNotThrow(() -> sut.validateCreateRequest(request)); } @Test - void validate_succeeds_when_location_is_null_or_empty() { + void validateCreateRequest_succeeds_when_location_is_null_or_empty() { CreateProjectRequest request = new CreateProjectRequest(); request.setProjectName("Valid"); request.setProjectFlavor("STANDARD"); request.setLocation(null); - assertDoesNotThrow(() -> sut.validate(request)); + assertDoesNotThrow(() -> sut.validateCreateRequest(request)); request.setLocation(""); - assertDoesNotThrow(() -> sut.validate(request)); + assertDoesNotThrow(() -> sut.validateCreateRequest(request)); request.setLocation(" "); - assertDoesNotThrow(() -> sut.validate(request)); + assertDoesNotThrow(() -> sut.validateCreateRequest(request)); } @Test - void validate_throws_exception_when_location_not_in_allowed_list() { + void validateCreateRequest_throws_exception_when_location_not_in_allowed_list() { CreateProjectRequest request = new CreateProjectRequest(); request.setProjectName("Valid"); request.setProjectFlavor("STANDARD"); @@ -131,7 +132,7 @@ void validate_throws_exception_when_location_not_in_allowed_list() { request.setLocation("INVALID_LOCATION_NOT_ALLOWED"); ProjectValidationException exception = - assertThrows(ProjectValidationException.class, () -> sut.validate(request)); + assertThrows(ProjectValidationException.class, () -> sut.validateCreateRequest(request)); assertEquals(ErrorKey.INVALID_LOCATION, exception.getErrorKey()); } @@ -142,12 +143,60 @@ void validate_throws_exception_when_location_not_in_allowed_list() { "BARCELONA", "SANT_CUGAT" }) - void validate_succeeds_when_location_is_allowed(String location) { + void validateCreateRequest_succeeds_when_location_is_allowed(String location) { CreateProjectRequest request = new CreateProjectRequest(); request.setProjectName("Valid"); request.setProjectFlavor("STANDARD"); request.setLocation(location); - assertDoesNotThrow(() -> sut.validate(request)); + assertDoesNotThrow(() -> sut.validateCreateRequest(request)); + } + + @Test + void validateUpdateRequest_succeeds_when_status_is_null() { + UpdateProjectRequest request = new UpdateProjectRequest(); + request.setStatus(null); + + assertDoesNotThrow(() -> sut.validateUpdateRequest(request)); + } + + @Test + void validateUpdateRequest_succeeds_when_status_is_empty_string() { + UpdateProjectRequest request = new UpdateProjectRequest(); + request.setStatus(""); + + assertDoesNotThrow(() -> sut.validateUpdateRequest(request)); + } + + @Test + void validateUpdateRequest_succeeds_when_status_is_blank() { + UpdateProjectRequest request = new UpdateProjectRequest(); + request.setStatus(" "); + + assertDoesNotThrow(() -> sut.validateUpdateRequest(request)); + } + + @ParameterizedTest + @CsvSource({ + "Pending", + "Running", + "Failed" + }) + void validateUpdateRequest_succeeds_when_status_is_valid(String status) { + UpdateProjectRequest request = new UpdateProjectRequest(); + request.setStatus(status); + + assertDoesNotThrow(() -> sut.validateUpdateRequest(request)); + } + + @Test + void validateUpdateRequest_throws_exception_with_error_key_INVALID_STATUS_when_status_is_invalid() { + UpdateProjectRequest request = new UpdateProjectRequest(); + request.setStatus("INVALID_STATUS"); + + ProjectValidationException exception = + assertThrows(ProjectValidationException.class, () -> sut.validateUpdateRequest(request)); + + assertEquals(ErrorKey.INVALID_STATUS, exception.getErrorKey()); } } \ No newline at end of file diff --git a/application.yaml b/application.yaml index 84b0ad73..2df8da94 100644 --- a/application.yaml +++ b/application.yaml @@ -222,6 +222,7 @@ apis: ansible-workflow-name: ${API_PROJECTS_MINIEDP_PROVISION_WORKFLOW_NAME} # Supported project locations, typically provided as a comma-separated environment variable. locations: ${API_PROJECTS_LOCATIONS} + status: ${API_PROJECTS_STATUS} externalservices: openshift: diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java index 3a23a8a4..896e7c6d 100644 --- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java +++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java @@ -10,6 +10,8 @@ public interface ProjectService { ProjectResponse saveProject(ProjectRequest request); ProjectResponse getProject(String projectKey); + + void updateProjectStatus(String projectKey, String status); List findProjectsByName(String projectName); } diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java index 7673925b..7140c321 100644 --- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java +++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java @@ -62,6 +62,18 @@ public ProjectResponse getProject(String projectKey) { return null; } + @Override + public void updateProjectStatus(String projectKey, String status) { + Optional project = projectRepository.findByProjectKeyIgnoreCase(projectKey); + + if (project.isPresent()) { + ProjectEntity entity = project.get(); + entity.setStatus(status); + projectRepository.save(entity); + } + + } + @Override public List findProjectsByName(String projectName) { List projects = projectRepository.findByProjectNameIgnoreCase(projectName); From 635ecf6c491831319e3e9c77655bb2217ef3ba85 Mon Sep 17 00:00:00 2001 From: Joselee2908 Date: Tue, 23 Jun 2026 12:00:16 +0200 Subject: [PATCH 2/5] Implement update project status endpoint and add corresponding tests --- api-project/openapi/api-project-internal.yaml | 39 +------ .../validation/ProjectRequestValidator.java | 3 - .../ProjectInternalControllerTest.java | 105 ++++++++++++++++++ .../facade/impl/ProjectsFacadeImplTest.java | 33 ++++++ .../service/impl/ProjectServiceImplTest.java | 45 +++++++- 5 files changed, 183 insertions(+), 42 deletions(-) create mode 100644 api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectInternalControllerTest.java diff --git a/api-project/openapi/api-project-internal.yaml b/api-project/openapi/api-project-internal.yaml index f1d5d01d..05916435 100644 --- a/api-project/openapi/api-project-internal.yaml +++ b/api-project/openapi/api-project-internal.yaml @@ -40,28 +40,12 @@ paths: description: Project information updated successfully. '404': description: Project not found. - content: - application/json: - schema: - $ref: '#/components/schemas/CreateProjectResponse' "401": description: Invalid client token on the request. - content: - application/json: - schema: - $ref: '#/components/schemas/RestErrorMessage' "403": description: Insufficient permissions for the client to access the resource. - content: - application/json: - schema: - $ref: '#/components/schemas/RestErrorMessage' '500': description: Internal server error. - content: - application/json: - schema: - $ref: '#/components/schemas/CreateProjectResponse' components: schemas: RestErrorMessage: @@ -76,25 +60,4 @@ components: status: type: string description: Status of the project. If not in the allowed list, error INVALID_STATUS (030) is returned. - additionalProperties: false - CreateProjectResponse: - type: object - properties: - projectKey: - type: string - httStatus: - type: string - status: - type: string - projectFlavor: - type: string - message: - type: string - error: - type: string - errorKey: - type: string - errorDescription: - type: string - location: - type: string \ No newline at end of file + additionalProperties: false \ No newline at end of file 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 b53e64f7..91c11ca6 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 @@ -16,9 +16,6 @@ public class ProjectRequestValidator { @Value("${apis.projects.locations}") private List locations; - - @Value("${apis.projects.status}") - private List statusList; public void validateCreateRequest(CreateProjectRequest request) { validateFlavorOrConfigItem(request); diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectInternalControllerTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectInternalControllerTest.java new file mode 100644 index 00000000..6be26b65 --- /dev/null +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectInternalControllerTest.java @@ -0,0 +1,105 @@ +package org.opendevstack.apiservice.project.controller; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.opendevstack.apiservice.project.exception.ErrorKey; +import org.opendevstack.apiservice.project.exception.ProjectValidationException; +import org.opendevstack.apiservice.project.facade.ProjectsFacade; +import org.opendevstack.apiservice.project.model.UpdateProjectRequest; +import org.opendevstack.apiservice.project.validation.ProjectRequestValidator; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ProjectInternalControllerTest { + + @Mock + private ProjectsFacade projectsFacade; + + @Mock + private ProjectRequestValidator projectRequestValidator; + + private ProjectInternalController sut; + private AutoCloseable mocks; + + @BeforeEach + void setup() { + mocks = MockitoAnnotations.openMocks(this); + sut = new ProjectInternalController(projectsFacade, projectRequestValidator); + + Jwt jwtToken = Jwt.withTokenValue("dummy-token") + .claim("appid", UUID.randomUUID().toString()) + .claim("sub", "test-user") + .header("alg", "none") + .build(); + JwtAuthenticationToken authentication = new JwtAuthenticationToken(jwtToken); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + @AfterEach + void tearDown() throws Exception { + mocks.close(); + } + + @Test + void update_project_returns_no_content_when_project_exists() { + UpdateProjectRequest request = new UpdateProjectRequest(); + request.setStatus("Running"); + + when(projectsFacade.updateProject("PROJ01", request)).thenReturn(true); + + ResponseEntity result = sut.updateProject("PROJ01", request); + + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); + assertThat(result.getBody()).isNull(); + assertThat(result.getHeaders().getFirst("Location")) + .isEqualTo(ProjectInternalController.API_BASE_PATH + "/PROJ01"); + verify(projectRequestValidator).validateUpdateRequest(request); + verify(projectsFacade).updateProject("PROJ01", request); + } + + @Test + void update_project_returns_not_found_when_project_does_not_exist() { + UpdateProjectRequest request = new UpdateProjectRequest(); + request.setStatus("Running"); + + when(projectsFacade.updateProject("UNKNOWN", request)).thenReturn(false); + + ResponseEntity result = sut.updateProject("UNKNOWN", request); + + assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(result.getBody()).isNull(); + assertThat(result.getHeaders().getFirst("Location")) + .isEqualTo(ProjectInternalController.API_BASE_PATH + "/UNKNOWN"); + verify(projectRequestValidator).validateUpdateRequest(request); + verify(projectsFacade).updateProject("UNKNOWN", request); + } + + @Test + void update_project_propagates_validation_exception_when_validator_throws() { + UpdateProjectRequest request = new UpdateProjectRequest(); + request.setStatus("INVALID_STATUS"); + + doThrow(new ProjectValidationException(ErrorKey.INVALID_STATUS)) + .when(projectRequestValidator).validateUpdateRequest(request); + + assertThrows(ProjectValidationException.class, () -> sut.updateProject("PROJ01", request)); + + verify(projectRequestValidator).validateUpdateRequest(request); + verify(projectsFacade, never()).updateProject("PROJ01", request); + } +} diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java index a81646cc..9c839074 100644 --- a/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java @@ -14,6 +14,7 @@ import org.opendevstack.apiservice.project.mapper.ProjectMapper; import org.opendevstack.apiservice.project.model.CreateProjectRequest; import org.opendevstack.apiservice.project.model.CreateProjectResponse; +import org.opendevstack.apiservice.project.model.UpdateProjectRequest; import org.opendevstack.apiservice.project.service.ClientAppService; import org.opendevstack.apiservice.serviceproject.model.ProjectRequest; import org.opendevstack.apiservice.serviceproject.model.ProjectResponse; @@ -208,4 +209,36 @@ void get_project_returns_null_when_service_returns_null() { assertNull(result); } + + @Test + void update_project_returns_true_and_calls_update_status_when_project_exists() { + UpdateProjectRequest request = new UpdateProjectRequest(); + request.setStatus("Running"); + ProjectResponse projectResponse = ProjectResponse.builder() + .projectKey("PROJ01") + .status(Status.PENDING) + .build(); + + when(projectService.getProject("PROJ01")).thenReturn(projectResponse); + + boolean result = sut.updateProject("PROJ01", request); + + assertEquals(true, result); + verify(projectService).getProject("PROJ01"); + verify(projectService).updateProjectStatus("PROJ01", "Running"); + } + + @Test + void update_project_returns_false_and_does_not_call_update_status_when_project_not_found() { + UpdateProjectRequest request = new UpdateProjectRequest(); + request.setStatus("Running"); + + when(projectService.getProject("UNKNOWN")).thenReturn(null); + + boolean result = sut.updateProject("UNKNOWN", request); + + assertEquals(false, result); + verify(projectService).getProject("UNKNOWN"); + verify(projectService, never()).updateProjectStatus(anyString(), anyString()); + } } diff --git a/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java b/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java index cadb492f..ca30c712 100644 --- a/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java +++ b/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java @@ -3,6 +3,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; +import org.mockito.ArgumentCaptor; import org.mockito.MockitoAnnotations; import org.opendevstack.apiservice.persistence.entity.ProjectEntity; import org.opendevstack.apiservice.persistence.repository.ProjectRepository; @@ -189,4 +190,46 @@ void find_projects_by_name_returns_empty_list_when_project_does_not_exist() { verify(projectRepository).findByProjectNameIgnoreCase(projectName); verify(projectResponseMapper).toCreateProjectResponse(anyList()); } -} \ No newline at end of file + + @Test + void update_project_status_saves_entity_with_new_status_when_project_exists() { + String projectKey = "PROJ01"; + ProjectEntity entity = ProjectEntity.builder() + .id(UUID.randomUUID()) + .projectKey(projectKey) + .projectName("My Project") + .status("Pending") + .build(); + + when(projectRepository.findByProjectKeyIgnoreCase(projectKey)).thenReturn(Optional.of(entity)); + + projectService.updateProjectStatus(projectKey, "Running"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ProjectEntity.class); + verify(projectRepository).save(captor.capture()); + assertEquals("Running", captor.getValue().getStatus()); + } + + @Test + void update_project_status_does_nothing_when_project_not_found() { + String projectKey = "UNKNOWN"; + + when(projectRepository.findByProjectKeyIgnoreCase(projectKey)).thenReturn(Optional.empty()); + + projectService.updateProjectStatus(projectKey, "Running"); + + verify(projectRepository, never()).save(any(ProjectEntity.class)); + } + + @Test + void update_project_status_propagates_repository_exception() { + String projectKey = "ERROR"; + + when(projectRepository.findByProjectKeyIgnoreCase(projectKey)) + .thenThrow(new RuntimeException("DB error")); + + assertThrows(RuntimeException.class, () -> projectService.updateProjectStatus(projectKey, "Running")); + + verify(projectRepository).findByProjectKeyIgnoreCase(projectKey); + } +} From 2e753df81099eaab7f66dada3c28244ab003ce9e Mon Sep 17 00:00:00 2001 From: Joselee2908 Date: Tue, 23 Jun 2026 12:49:43 +0200 Subject: [PATCH 3/5] Enhance update project status endpoint with improved response handling and validation --- api-project/openapi/api-project-internal.yaml | 6 +++--- .../controller/ProjectInternalController.java | 11 ++-------- .../advice/ProjectExceptionHandler.java | 7 +++++++ .../ProjectUpdateValidationException.java | 19 +++++++++++++++++ .../project/facade/ProjectsFacade.java | 2 +- .../facade/impl/ProjectsFacadeImpl.java | 6 +----- .../validation/ProjectRequestValidator.java | 3 ++- .../ProjectInternalControllerTest.java | 21 +------------------ .../facade/impl/ProjectsFacadeImplTest.java | 20 ++---------------- .../ProjectRequestValidatorTest.java | 5 +++-- .../service/impl/ProjectServiceImpl.java | 1 - 11 files changed, 41 insertions(+), 60 deletions(-) create mode 100644 api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectUpdateValidationException.java diff --git a/api-project/openapi/api-project-internal.yaml b/api-project/openapi/api-project-internal.yaml index 05916435..74a1bb1b 100644 --- a/api-project/openapi/api-project-internal.yaml +++ b/api-project/openapi/api-project-internal.yaml @@ -37,9 +37,9 @@ paths: $ref: '#/components/schemas/UpdateProjectRequest' responses: '204': - description: Project information updated successfully. - '404': - description: Project not found. + description: Project information updated successfully or no project found for the provided projectKey. + "400": + description: Bad Request. "401": description: Invalid client token on the request. "403": diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectInternalController.java b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectInternalController.java index 88691d8f..120d9e4c 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectInternalController.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectInternalController.java @@ -40,15 +40,8 @@ public ResponseEntity updateProject(@PathVariable String projectKey, String location = API_BASE_PATH + "/" + projectKey; projectRequestValidator.validateUpdateRequest(updateProjectRequest); - Boolean updated = projectsFacade.updateProject(projectKey, updateProjectRequest); - - if (updated.equals(Boolean.FALSE)) { - return ResponseEntity - .status(HttpStatus.NOT_FOUND) - .header(HTTP_HEADER_LOCATION, location) - .build(); - // .body(ProjectResponseFactory.notFound(projectKey, location)); - } + projectsFacade.updateProject(projectKey, updateProjectRequest); + return ResponseEntity .status(HttpStatus.NO_CONTENT) .header(HTTP_HEADER_LOCATION, location) 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 e9170065..221d85ab 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 @@ -7,6 +7,7 @@ import org.opendevstack.apiservice.project.exception.ErrorKey; import org.opendevstack.apiservice.project.exception.ProjectAlreadyExistsException; import org.opendevstack.apiservice.project.exception.ProjectCreationException; +import org.opendevstack.apiservice.project.exception.ProjectUpdateValidationException; import org.opendevstack.apiservice.project.exception.ProjectValidationException; import org.opendevstack.apiservice.project.model.CreateProjectResponse; import org.springframework.http.HttpStatus; @@ -86,6 +87,12 @@ public ResponseEntity handleValidationException(ProjectVa return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); } + @ExceptionHandler(ProjectUpdateValidationException.class) + public ResponseEntity handleUpdateValidationException(ProjectUpdateValidationException ex) { + log.warn("Validation error: {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + @ExceptionHandler(ProjectAlreadyExistsException.class) public ResponseEntity handleProjectAlreadyExistsException(ProjectAlreadyExistsException ex) { log.warn("Validation error: {}", ex.getMessage()); diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectUpdateValidationException.java b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectUpdateValidationException.java new file mode 100644 index 00000000..56f0b5b2 --- /dev/null +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectUpdateValidationException.java @@ -0,0 +1,19 @@ +package org.opendevstack.apiservice.project.exception; + +import lombok.Getter; + +@Getter +public class ProjectUpdateValidationException extends RuntimeException { + + private final ErrorKey errorKey; + + public ProjectUpdateValidationException(ErrorKey errorKey) { + super(errorKey.getMessage()); + this.errorKey = errorKey; + } + + public ProjectUpdateValidationException(ErrorKey errorKey, String additionalMessage) { + super(errorKey.getMessage() + " " + additionalMessage); + this.errorKey = errorKey; + } +} diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/facade/ProjectsFacade.java b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/ProjectsFacade.java index 9424c223..f36aa8dc 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/facade/ProjectsFacade.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/ProjectsFacade.java @@ -12,5 +12,5 @@ public interface ProjectsFacade { CreateProjectResponse getProject(String projectKey); - boolean updateProject(String projectKey, UpdateProjectRequest request); + void updateProject(String projectKey, UpdateProjectRequest request); } \ No newline at end of file diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImpl.java b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImpl.java index 36fad5dc..afa845df 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImpl.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImpl.java @@ -86,12 +86,8 @@ public CreateProjectResponse getProject(String projectKey) { } @Override - public boolean updateProject(String projectKey, UpdateProjectRequest request) { - ProjectResponse project = projectService.getProject(projectKey); - if (project == null) return false; - + public void updateProject(String projectKey, UpdateProjectRequest request) { projectService.updateProjectStatus(projectKey, request.getStatus()); - return true; } private ProjectResponse registerProject(ProjectCreationCommand command) { 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 91c11ca6..c96d32f9 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 @@ -1,6 +1,7 @@ package org.opendevstack.apiservice.project.validation; import org.opendevstack.apiservice.project.exception.ErrorKey; +import org.opendevstack.apiservice.project.exception.ProjectUpdateValidationException; import org.opendevstack.apiservice.project.exception.ProjectValidationException; import org.opendevstack.apiservice.project.model.CreateProjectRequest; import org.opendevstack.apiservice.project.model.UpdateProjectRequest; @@ -79,7 +80,7 @@ private void validateStatus(UpdateProjectRequest request) { .map(e -> e.getDbValue()) .toList(); if (!statusList.contains(status)) { - throw new ProjectValidationException(ErrorKey.INVALID_STATUS, + throw new ProjectUpdateValidationException(ErrorKey.INVALID_STATUS, String.join(", ", statusList)); } } diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectInternalControllerTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectInternalControllerTest.java index 6be26b65..efcfc848 100644 --- a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectInternalControllerTest.java +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectInternalControllerTest.java @@ -56,12 +56,10 @@ void tearDown() throws Exception { } @Test - void update_project_returns_no_content_when_project_exists() { + void update_project_returns_no_content() { UpdateProjectRequest request = new UpdateProjectRequest(); request.setStatus("Running"); - when(projectsFacade.updateProject("PROJ01", request)).thenReturn(true); - ResponseEntity result = sut.updateProject("PROJ01", request); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NO_CONTENT); @@ -72,23 +70,6 @@ void update_project_returns_no_content_when_project_exists() { verify(projectsFacade).updateProject("PROJ01", request); } - @Test - void update_project_returns_not_found_when_project_does_not_exist() { - UpdateProjectRequest request = new UpdateProjectRequest(); - request.setStatus("Running"); - - when(projectsFacade.updateProject("UNKNOWN", request)).thenReturn(false); - - ResponseEntity result = sut.updateProject("UNKNOWN", request); - - assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); - assertThat(result.getBody()).isNull(); - assertThat(result.getHeaders().getFirst("Location")) - .isEqualTo(ProjectInternalController.API_BASE_PATH + "/UNKNOWN"); - verify(projectRequestValidator).validateUpdateRequest(request); - verify(projectsFacade).updateProject("UNKNOWN", request); - } - @Test void update_project_propagates_validation_exception_when_validator_throws() { UpdateProjectRequest request = new UpdateProjectRequest(); diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java index 9c839074..abf00d34 100644 --- a/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java @@ -211,7 +211,7 @@ void get_project_returns_null_when_service_returns_null() { } @Test - void update_project_returns_true_and_calls_update_status_when_project_exists() { + void update_project_calls_update_status() { UpdateProjectRequest request = new UpdateProjectRequest(); request.setStatus("Running"); ProjectResponse projectResponse = ProjectResponse.builder() @@ -221,24 +221,8 @@ void update_project_returns_true_and_calls_update_status_when_project_exists() { when(projectService.getProject("PROJ01")).thenReturn(projectResponse); - boolean result = sut.updateProject("PROJ01", request); + sut.updateProject("PROJ01", request); - assertEquals(true, result); - verify(projectService).getProject("PROJ01"); verify(projectService).updateProjectStatus("PROJ01", "Running"); } - - @Test - void update_project_returns_false_and_does_not_call_update_status_when_project_not_found() { - UpdateProjectRequest request = new UpdateProjectRequest(); - request.setStatus("Running"); - - when(projectService.getProject("UNKNOWN")).thenReturn(null); - - boolean result = sut.updateProject("UNKNOWN", request); - - assertEquals(false, result); - verify(projectService).getProject("UNKNOWN"); - verify(projectService, never()).updateProjectStatus(anyString(), anyString()); - } } diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidatorTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidatorTest.java index f8265926..e2fed8c2 100644 --- a/api-project/src/test/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidatorTest.java +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidatorTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.opendevstack.apiservice.project.exception.ErrorKey; +import org.opendevstack.apiservice.project.exception.ProjectUpdateValidationException; import org.opendevstack.apiservice.project.exception.ProjectValidationException; import org.opendevstack.apiservice.project.model.CreateProjectRequest; import org.opendevstack.apiservice.project.model.UpdateProjectRequest; @@ -194,8 +195,8 @@ void validateUpdateRequest_throws_exception_with_error_key_INVALID_STATUS_when_s UpdateProjectRequest request = new UpdateProjectRequest(); request.setStatus("INVALID_STATUS"); - ProjectValidationException exception = - assertThrows(ProjectValidationException.class, () -> sut.validateUpdateRequest(request)); + ProjectUpdateValidationException exception = + assertThrows(ProjectUpdateValidationException.class, () -> sut.validateUpdateRequest(request)); assertEquals(ErrorKey.INVALID_STATUS, exception.getErrorKey()); } diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java index 7140c321..fe9b34e3 100644 --- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java +++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java @@ -71,7 +71,6 @@ public void updateProjectStatus(String projectKey, String status) { entity.setStatus(status); projectRepository.save(entity); } - } @Override From 23bfac683282227217fba8107547ce8670c792c2 Mon Sep 17 00:00:00 2001 From: Joselee2908 Date: Tue, 23 Jun 2026 14:28:28 +0200 Subject: [PATCH 4/5] Add exception handler for project update validation and update API version --- .../controller/ProjectInternalController.java | 2 +- .../advice/ProjectExceptionHandler.java | 7 --- .../ProjectInternalExceptionHandler.java | 22 ++++++++ .../ProjectInternalExceptionHandlerTest.java | 53 +++++++++++++++++++ 4 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 api-project/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectInternalExceptionHandler.java create mode 100644 api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectInternalExceptionHandlerTest.java diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectInternalController.java b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectInternalController.java index 120d9e4c..af68367e 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectInternalController.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectInternalController.java @@ -25,7 +25,7 @@ @Slf4j public class ProjectInternalController implements ProjectsInternalApi { - public static final String API_BASE_PATH = "/api/pub/v0/projects"; + public static final String API_BASE_PATH = "/api/pub/v1/projects"; private static final String HTTP_HEADER_LOCATION = "Location"; 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 221d85ab..e9170065 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 @@ -7,7 +7,6 @@ import org.opendevstack.apiservice.project.exception.ErrorKey; import org.opendevstack.apiservice.project.exception.ProjectAlreadyExistsException; import org.opendevstack.apiservice.project.exception.ProjectCreationException; -import org.opendevstack.apiservice.project.exception.ProjectUpdateValidationException; import org.opendevstack.apiservice.project.exception.ProjectValidationException; import org.opendevstack.apiservice.project.model.CreateProjectResponse; import org.springframework.http.HttpStatus; @@ -87,12 +86,6 @@ public ResponseEntity handleValidationException(ProjectVa return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response); } - @ExceptionHandler(ProjectUpdateValidationException.class) - public ResponseEntity handleUpdateValidationException(ProjectUpdateValidationException ex) { - log.warn("Validation error: {}", ex.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); - } - @ExceptionHandler(ProjectAlreadyExistsException.class) public ResponseEntity handleProjectAlreadyExistsException(ProjectAlreadyExistsException ex) { log.warn("Validation error: {}", ex.getMessage()); diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectInternalExceptionHandler.java b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectInternalExceptionHandler.java new file mode 100644 index 00000000..ab8faab9 --- /dev/null +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectInternalExceptionHandler.java @@ -0,0 +1,22 @@ +package org.opendevstack.apiservice.project.controller.advice; + +import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.project.controller.ProjectInternalController; +import org.opendevstack.apiservice.project.exception.ProjectUpdateValidationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice(assignableTypes = ProjectInternalController.class) +@Slf4j +public class ProjectInternalExceptionHandler { + + @ExceptionHandler(ProjectUpdateValidationException.class) + public ResponseEntity handleUpdateValidationException(ProjectUpdateValidationException ex) { + log.warn("Validation error: {}", ex.getMessage()); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).build(); + } + + +} diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectInternalExceptionHandlerTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectInternalExceptionHandlerTest.java new file mode 100644 index 00000000..968c7670 --- /dev/null +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectInternalExceptionHandlerTest.java @@ -0,0 +1,53 @@ +package org.opendevstack.apiservice.project.controller.advice; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; +import org.opendevstack.apiservice.project.exception.ErrorKey; +import org.opendevstack.apiservice.project.exception.ProjectUpdateValidationException; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class ProjectInternalExceptionHandlerTest { + + private ProjectInternalExceptionHandler sut; + private AutoCloseable mocks; + + @BeforeEach + void setUp() { + mocks = MockitoAnnotations.openMocks(this); + sut = new ProjectInternalExceptionHandler(); + } + + @AfterEach + void tearDown() throws Exception { + mocks.close(); + } + + @Test + void handle_update_validation_exception_returns_bad_request_with_no_body() { + ProjectUpdateValidationException exception = + new ProjectUpdateValidationException(ErrorKey.INVALID_STATUS); + + ResponseEntity result = sut.handleUpdateValidationException(exception); + + assertEquals(HttpStatus.BAD_REQUEST, result.getStatusCode()); + assertNull(result.getBody()); + } + + @Test + void handle_update_validation_exception_with_additional_message_returns_bad_request_with_no_body() { + ProjectUpdateValidationException exception = + new ProjectUpdateValidationException(ErrorKey.INVALID_STATUS, "Pending,Running,Failed"); + + ResponseEntity result = sut.handleUpdateValidationException(exception); + + assertEquals(HttpStatus.BAD_REQUEST, result.getStatusCode()); + assertNull(result.getBody()); + } + +} From 3406e62b7fc4bff8db96c069d3b7a1bb4cb843f2 Mon Sep 17 00:00:00 2001 From: Joselee2908 Date: Tue, 23 Jun 2026 16:58:24 +0200 Subject: [PATCH 5/5] Add initial API definition for Project API v1 and remove status configuration --- application.yaml | 1 - .../migrations/006_add_projects_internal_api.sql | 12 ++++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 persistence/src/main/resources/db/changelog/migrations/006_add_projects_internal_api.sql diff --git a/application.yaml b/application.yaml index 2df8da94..84b0ad73 100644 --- a/application.yaml +++ b/application.yaml @@ -222,7 +222,6 @@ apis: ansible-workflow-name: ${API_PROJECTS_MINIEDP_PROVISION_WORKFLOW_NAME} # Supported project locations, typically provided as a comma-separated environment variable. locations: ${API_PROJECTS_LOCATIONS} - status: ${API_PROJECTS_STATUS} externalservices: openshift: diff --git a/persistence/src/main/resources/db/changelog/migrations/006_add_projects_internal_api.sql b/persistence/src/main/resources/db/changelog/migrations/006_add_projects_internal_api.sql new file mode 100644 index 00000000..d5daa7de --- /dev/null +++ b/persistence/src/main/resources/db/changelog/migrations/006_add_projects_internal_api.sql @@ -0,0 +1,12 @@ +--liquibase formatted sql + +-- ============================================================================ +-- changeset ods:009-add-project-v1-api-definition +-- Description: Seed API definitions for Project API v1. +-- ============================================================================ +INSERT INTO api_definitions (api_id, name, base_path, version, auth_types, is_public, enabled) +VALUES + ('project-v1', 'Project API v1', 'projects', 'v1', ARRAY['CLIENT_CREDENTIALS'], FALSE, TRUE) + ON CONFLICT (api_id) DO NOTHING; + +--rollback DELETE FROM api_definitions WHERE api_id IN ('project-v1'); \ No newline at end of file