diff --git a/api-project/openapi/api-project.yaml b/api-project/openapi/api-project.yaml
index 8fbd57b..9c83d30 100644
--- a/api-project/openapi/api-project.yaml
+++ b/api-project/openapi/api-project.yaml
@@ -142,6 +142,8 @@ components:
type: string
status:
type: string
+ projectFlavor:
+ type: string
message:
type: string
error:
@@ -150,3 +152,5 @@ components:
type: string
errorDescription:
type: string
+ location:
+ type: string
diff --git a/api-project/pom.xml b/api-project/pom.xml
index 8daceb0..e77eda9 100644
--- a/api-project/pom.xml
+++ b/api-project/pom.xml
@@ -74,7 +74,7 @@
org.mapstruct
mapstruct
- 1.6.3
+ ${mapstruct.version}
@@ -111,7 +111,7 @@
org.mapstruct
mapstruct-processor
- 1.6.3
+ ${mapstruct.version}
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 a17d338..fe8012d 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
@@ -26,38 +26,51 @@ public class ProjectController implements ProjectsApi {
public static final String API_BASE_PATH = "/api/pub/v0/projects";
+ private static final String HTTP_HEADER_LOCATION = "Location";
+
private final ProjectsFacade projectsFacade;
@PostMapping
@Override
public ResponseEntity createProject(@Valid @RequestBody CreateProjectRequest createProjectRequest) {
try {
- return ResponseEntity.ok(projectsFacade.createProject(createProjectRequest));
+ return ResponseEntity
+ .status(HttpStatus.OK)
+ .header(HTTP_HEADER_LOCATION, API_BASE_PATH)
+ .body(projectsFacade.createProject(createProjectRequest));
} catch (ProjectCreationException e) {
log.error("Project creation conflict: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT)
- .body(ProjectResponseFactory.conflict(e.getMessage()));
+ .header(HTTP_HEADER_LOCATION, API_BASE_PATH)
+ .body(ProjectResponseFactory.conflict(e.getMessage(), API_BASE_PATH));
} catch (ProjectKeyGenerationException e) {
log.error("Failed to generate project key: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
- .body(ProjectResponseFactory.projectKeyGenerationFailed());
+ .header(HTTP_HEADER_LOCATION, API_BASE_PATH)
+ .body(ProjectResponseFactory.projectKeyGenerationFailed(API_BASE_PATH));
}
}
@GetMapping("/{projectKey}")
@Override
public ResponseEntity getProject(@PathVariable String projectKey) {
+ String location = API_BASE_PATH + "/" + projectKey;
try {
CreateProjectResponse response = projectsFacade.getProject(projectKey);
if (response == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
- .body(ProjectResponseFactory.notFound(projectKey));
+ .header(HTTP_HEADER_LOCATION, location)
+ .body(ProjectResponseFactory.notFound(projectKey, location));
}
- return ResponseEntity.ok(response);
- } catch (ProjectCreationException e) {
- log.error("Error retrieving project '{}': {}", projectKey, e.getMessage(), e);
+ return ResponseEntity
+ .status(HttpStatus.OK)
+ .header(HTTP_HEADER_LOCATION, location)
+ .body(response);
+ } catch (Exception e) {
+ log.error("Unexpected error retrieving project '{}': {}", projectKey, e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
- .body(ProjectResponseFactory.internalError());
+ .header(HTTP_HEADER_LOCATION, location)
+ .body(ProjectResponseFactory.internalError(location));
}
}
}
diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java
index 6a6faae..b1ed5d2 100644
--- a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java
+++ b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java
@@ -1,38 +1,50 @@
package org.opendevstack.apiservice.project.controller;
+import org.opendevstack.apiservice.project.exception.ErrorKey;
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
public final class ProjectResponseFactory {
- private static final String INTERNAL_ERROR = "INTERNAL_ERROR";
-
private ProjectResponseFactory() {
}
- public static CreateProjectResponse conflict(String message) {
- return error("CONFLICT", "PROJECT_ALREADY_EXISTS", message);
+ public static CreateProjectResponse conflict(String message, String location) {
+ return error(
+ ErrorKey.PROJECT_ALREADY_EXISTS.getMessage(),
+ ErrorKey.PROJECT_ALREADY_EXISTS.getKey(),
+ message, location);
}
- public static CreateProjectResponse projectKeyGenerationFailed() {
- return error(INTERNAL_ERROR, "PROJECT_KEY_GENERATION_FAILED",
- "Failed to generate a unique project key.");
+ public static CreateProjectResponse projectKeyGenerationFailed(String location) {
+ return error(ErrorKey.INTERNAL_ERROR.getMessage(),
+ "PROJECT_KEY_GENERATION_FAILED",
+ "Failed to generate a unique project key.",
+ location);
}
- public static CreateProjectResponse notFound(String projectKey) {
- return error("NOT_FOUND", "PROJECT_NOT_FOUND",
- String.format("Project with key '%s' not found", projectKey));
+ public static CreateProjectResponse notFound(String projectKey, String location) {
+ return error(
+ ErrorKey.PROJECT_NOT_FOUND.getMessage(),
+ ErrorKey.PROJECT_NOT_FOUND.getKey(),
+ String.format("Project with key '%s' not found", projectKey),
+ location
+ );
}
- public static CreateProjectResponse internalError() {
- return error(INTERNAL_ERROR, INTERNAL_ERROR,
- "An error occurred while processing the request.");
+ public static CreateProjectResponse internalError(String location) {
+ return error(
+ ErrorKey.INTERNAL_ERROR.getMessage(),
+ ErrorKey.INTERNAL_ERROR.getKey(),
+ "An error occurred while processing the request.",
+ location);
}
- private static CreateProjectResponse error(String error, String errorKey, String message) {
+ private static CreateProjectResponse error(String error, String errorKey, String message, String location) {
CreateProjectResponse response = new CreateProjectResponse();
response.setError(error);
response.setErrorKey(errorKey);
response.setMessage(message);
+ response.setLocation(location);
return response;
}
}
\ No newline at end of file
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
new file mode 100644
index 0000000..4ee644d
--- /dev/null
+++ b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java
@@ -0,0 +1,48 @@
+package org.opendevstack.apiservice.project.exception;
+
+public enum ErrorKey {
+
+ OK("000", "Success"),
+ PRODUCT_NOT_FOUND("001", ErrorMessage.NOT_FOUND),
+ ACCESS_DENIED("002", ErrorMessage.FORBIDDEN),
+ INTERNAL_ERROR("003", "Internal error"),
+ INVALID_AUTH_HEADER("004", ErrorMessage.BAD_REQUEST),
+ MISSING_AUTH_HEADER("005", ErrorMessage.BAD_REQUEST),
+ INVALID_PARAMETERS("006", ErrorMessage.BAD_REQUEST),
+ X2_ACCOUNT_MISSING_GROUPS("007", ErrorMessage.NOT_FOUND),
+ 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),
+ 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"),
+ 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");
+
+ private String key;
+ private String message;
+
+ ErrorKey(String key, String message) {
+ this.key = key;
+ this.message = message;
+ }
+
+ public String getKey() {
+ return this.key;
+ }
+
+ public String getMessage() {
+ return this.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
new file mode 100644
index 0000000..767ab2e
--- /dev/null
+++ b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorMessage.java
@@ -0,0 +1,12 @@
+package org.opendevstack.apiservice.project.exception;
+
+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";
+
+ private ErrorMessage() {
+ // prevent instantiation
+ }
+}
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 9d29f13..b30ad4f 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
@@ -10,5 +10,5 @@ public interface ProjectsFacade {
CreateProjectResponse createProject(CreateProjectRequest request)
throws ProjectCreationException, ProjectKeyGenerationException;
- CreateProjectResponse getProject(String projectKey) throws ProjectCreationException;
+ CreateProjectResponse getProject(String projectKey);
}
\ 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 274ad1d..ea66ee8 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
@@ -29,7 +29,7 @@ public CreateProjectResponse createProject(CreateProjectRequest request)
}
@Override
- public CreateProjectResponse getProject(String projectKey) throws ProjectCreationException {
+ public CreateProjectResponse getProject(String projectKey) {
return projectMapper.toApiResponse(projectService.getProject(projectKey));
}
}
\ No newline at end of file
diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java b/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java
index f9b24c8..181b571 100644
--- a/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java
+++ b/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java
@@ -1,15 +1,49 @@
package org.opendevstack.apiservice.project.mapper;
import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Named;
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
+import org.opendevstack.apiservice.serviceproject.model.ProjectRequest;
+import org.opendevstack.apiservice.serviceproject.model.ProjectResponse;
+import org.opendevstack.apiservice.serviceproject.model.Status;
+
+import java.text.MessageFormat;
@Mapper(componentModel = "spring")
public interface ProjectMapper {
-
- org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest toServiceRequest(
- CreateProjectRequest apiRequest);
-
- CreateProjectResponse toApiResponse(
- org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse serviceResponse);
+
+ ProjectRequest toServiceRequest(CreateProjectRequest apiRequest);
+
+ @Mapping(source = "status", target = "status", qualifiedByName = "mapStatus")
+ @Mapping(source = "projectKey", target = "location", qualifiedByName = "mapLocation")
+ @Mapping(source = ".", target = "errorDescription", qualifiedByName = "mapErrorDescription")
+ CreateProjectResponse toApiResponse(ProjectResponse serviceResponse);
+
+ @Named("mapStatus")
+ default String mapStatus(Status status) {
+ if (status == null) {
+ return null;
+ }
+ return status.getDbValue();
+ }
+
+ @Named("mapErrorDescription")
+ default String mapErrorDescription(ProjectResponse serviceResponse) {
+ return (serviceResponse.getStatus() == Status.FAILED)
+ ? MessageFormat.format(
+ "There was an error when creating the project {0}.\n\n " +
+ "The error has been reported to our Support team as an incident. " +
+ "You will be informed about the incident via email.", serviceResponse.getProjectKey())
+ : null;
+ }
+
+ @Named("mapLocation")
+ default String mapLocation(String projectKey) {
+ if (projectKey == null || projectKey.isEmpty()) {
+ return null;
+ }
+ return "/api/pub/v0/projects/" + projectKey;
+ }
}
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 39f5fd9..5e57b72 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
@@ -51,6 +51,9 @@ void createProject_whenSuccess_thenReturnOk() throws Exception {
assertThat(result.getBody()).isNotNull();
assertThat(result.getBody().getProjectKey()).isEqualTo("PROJ01");
assertThat(result.getBody().getStatus()).isEqualTo("Initiated");
+ assertThat(result.getBody().getError()).isNull();
+ assertThat(result.getBody().getErrorKey()).isNull();
+ assertThat(result.getBody().getErrorDescription()).isNull();
}
@Test
@@ -65,9 +68,12 @@ void createProject_whenProjectCreationException_thenReturnConflict() throws Exce
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
assertThat(result.getBody()).isNotNull();
- assertThat(result.getBody().getError()).isEqualTo("CONFLICT");
- assertThat(result.getBody().getErrorKey()).isEqualTo("PROJECT_ALREADY_EXISTS");
- assertThat(result.getBody().getMessage()).contains("already exists");
+ assertThat(result.getBody().getError()).isEqualTo("Project already exists");
+ assertThat(result.getBody().getErrorKey()).isEqualTo("025");
+ assertThat(result.getBody().getMessage()).contains("Project with key 'EXISTING' already exists");
+ assertThat(result.getBody().getProjectKey()).isNull();
+ assertThat(result.getBody().getStatus()).isNull();
+ assertThat(result.getBody().getErrorDescription()).isNull();
}
@Test
@@ -81,9 +87,12 @@ void createProject_whenProjectKeyGenerationException_thenReturnInternalServerErr
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assertThat(result.getBody()).isNotNull();
- assertThat(result.getBody().getError()).isEqualTo("INTERNAL_ERROR");
+ assertThat(result.getBody().getError()).isEqualTo("Internal error");
assertThat(result.getBody().getErrorKey()).isEqualTo("PROJECT_KEY_GENERATION_FAILED");
assertThat(result.getBody().getMessage()).isEqualTo("Failed to generate a unique project key.");
+ assertThat(result.getBody().getProjectKey()).isNull();
+ assertThat(result.getBody().getStatus()).isNull();
+ assertThat(result.getBody().getErrorDescription()).isNull();
}
@Test
@@ -99,6 +108,8 @@ void getProject_whenFound_thenReturnOk() throws Exception {
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(result.getBody()).isNotNull();
assertThat(result.getBody().getProjectKey()).isEqualTo("PROJ01");
+ assertThat(result.getBody().getError()).isNull();
+ assertThat(result.getBody().getErrorKey()).isNull();
verify(projectsFacade).getProject("PROJ01");
}
@@ -110,23 +121,29 @@ void getProject_whenNotFound_thenReturnNotFound() throws Exception {
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(result.getBody()).isNotNull();
- assertThat(result.getBody().getError()).isEqualTo("NOT_FOUND");
- assertThat(result.getBody().getErrorKey()).isEqualTo("PROJECT_NOT_FOUND");
+ assertThat(result.getBody().getError()).isEqualTo("Not Found");
+ assertThat(result.getBody().getErrorKey()).isEqualTo("012");
assertThat(result.getBody().getMessage()).contains("UNKNOWN");
+ assertThat(result.getBody().getProjectKey()).isNull();
+ assertThat(result.getBody().getStatus()).isNull();
+ assertThat(result.getBody().getErrorDescription()).isNull();
}
@Test
void getProject_whenServiceThrows_thenReturnInternalServerError() throws Exception {
when(projectsFacade.getProject(anyString()))
- .thenThrow(new ProjectCreationException("Database error"));
+ .thenThrow(new RuntimeException("Database error"));
ResponseEntity result = sut.getProject("PROJ01");
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assertThat(result.getBody()).isNotNull();
- assertThat(result.getBody().getError()).isEqualTo("INTERNAL_ERROR");
- assertThat(result.getBody().getErrorKey()).isEqualTo("INTERNAL_ERROR");
+ assertThat(result.getBody().getError()).isEqualTo("Internal error");
+ assertThat(result.getBody().getErrorKey()).isEqualTo("003");
assertThat(result.getBody().getMessage()).isEqualTo("An error occurred while processing the request.");
+ assertThat(result.getBody().getProjectKey()).isNull();
+ assertThat(result.getBody().getStatus()).isNull();
+ assertThat(result.getBody().getErrorDescription()).isNull();
}
}
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 dc0cf00..c11a596 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
@@ -9,6 +9,9 @@
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.serviceproject.model.ProjectRequest;
+import org.opendevstack.apiservice.serviceproject.model.ProjectResponse;
+import org.opendevstack.apiservice.serviceproject.model.Status;
import org.opendevstack.apiservice.serviceproject.service.ProjectService;
import static org.assertj.core.api.Assertions.assertThat;
@@ -35,29 +38,29 @@ void createProject_whenServiceReturnsValue_thenMapToApiModel() throws Exception
CreateProjectRequest request = new CreateProjectRequest("My Project");
request.setProjectKey("PROJ01");
- org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse serviceResponse =
- new org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse();
+ ProjectResponse serviceResponse =
+ new ProjectResponse();
serviceResponse.setProjectKey("PROJ01");
- serviceResponse.setStatus("Initiated");
+ serviceResponse.setStatus(Status.PENDING);
when(projectService.createProject(org.mockito.ArgumentMatchers.any(
- org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest.class)))
+ ProjectRequest.class)))
.thenReturn(serviceResponse);
CreateProjectResponse response = sut.createProject(request);
assertThat(response).isNotNull();
assertThat(response.getProjectKey()).isEqualTo("PROJ01");
- assertThat(response.getStatus()).isEqualTo("Initiated");
+ assertThat(response.getStatus()).isEqualTo("Pending");
verify(projectService).createProject(org.mockito.ArgumentMatchers.any(
- org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest.class));
+ ProjectRequest.class));
}
@Test
void createProject_whenServiceReturnsNull_thenReturnNull() throws Exception {
CreateProjectRequest request = new CreateProjectRequest("My Project");
when(projectService.createProject(org.mockito.ArgumentMatchers.any(
- org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest.class)))
+ ProjectRequest.class)))
.thenReturn(null);
CreateProjectResponse response = sut.createProject(request);
@@ -67,10 +70,10 @@ void createProject_whenServiceReturnsNull_thenReturnNull() throws Exception {
@Test
void getProject_whenServiceReturnsValue_thenMapToApiModel() throws Exception {
- org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse serviceResponse =
- new org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse();
+ ProjectResponse serviceResponse =
+ new ProjectResponse();
serviceResponse.setProjectKey("PROJ01");
- serviceResponse.setStatus("Found");
+ serviceResponse.setStatus(Status.RUNNING);
when(projectService.getProject("PROJ01")).thenReturn(serviceResponse);
@@ -78,7 +81,7 @@ void getProject_whenServiceReturnsValue_thenMapToApiModel() throws Exception {
assertThat(response).isNotNull();
assertThat(response.getProjectKey()).isEqualTo("PROJ01");
- assertThat(response.getStatus()).isEqualTo("Found");
+ assertThat(response.getStatus()).isEqualTo("Running");
}
@Test
diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/mapper/ProjectMapperTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/mapper/ProjectMapperTest.java
new file mode 100644
index 0000000..b7cf95a
--- /dev/null
+++ b/api-project/src/test/java/org/opendevstack/apiservice/project/mapper/ProjectMapperTest.java
@@ -0,0 +1,207 @@
+package org.opendevstack.apiservice.project.mapper;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mapstruct.factory.Mappers;
+import org.mockito.MockitoAnnotations;
+import org.opendevstack.apiservice.project.model.CreateProjectRequest;
+import org.opendevstack.apiservice.project.model.CreateProjectResponse;
+import org.opendevstack.apiservice.serviceproject.model.ProjectRequest;
+import org.opendevstack.apiservice.serviceproject.model.ProjectResponse;
+import org.opendevstack.apiservice.serviceproject.model.Status;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class ProjectMapperTest {
+
+ private ProjectMapper projectMapper;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ projectMapper = Mappers.getMapper(ProjectMapper.class);
+ }
+
+ @Test
+ void to_service_request_maps_all_fields_correctly() {
+ CreateProjectRequest apiRequest = new CreateProjectRequest("My Project");
+ apiRequest.setProjectKey("PROJ01");
+ apiRequest.setProjectKeyPattern("SS%06d");
+ apiRequest.setProjectDescription("A test project");
+
+ ProjectRequest result = projectMapper.toServiceRequest(apiRequest);
+
+ assertNotNull(result);
+ assertEquals("PROJ01", result.getProjectKey());
+ assertEquals("SS%06d", result.getProjectKeyPattern());
+ assertEquals("My Project", result.getProjectName());
+ assertEquals("A test project", result.getProjectDescription());
+ }
+
+ @Test
+ void to_service_request_returns_null_when_input_is_null() {
+ CreateProjectRequest apiRequest = null;
+
+ ProjectRequest result = projectMapper.toServiceRequest(apiRequest);
+
+ assertNull(result);
+ }
+
+ @Test
+ void to_service_request_maps_only_required_field() {
+ CreateProjectRequest apiRequest = new CreateProjectRequest("Only Name");
+
+ ProjectRequest result = projectMapper.toServiceRequest(apiRequest);
+
+ assertNotNull(result);
+ assertNull(result.getProjectKey());
+ assertNull(result.getProjectKeyPattern());
+ assertEquals("Only Name", result.getProjectName());
+ assertNull(result.getProjectDescription());
+ }
+
+ @Test
+ void to_api_response_maps_all_fields_correctly() {
+ ProjectResponse serviceResponse = ProjectResponse.builder()
+ .projectKey("PROJ01")
+ .status(Status.PENDING)
+ .projectFlavor("AMP")
+ .build();
+
+ CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse);
+
+ assertNotNull(result);
+ assertEquals("PROJ01", result.getProjectKey());
+ assertEquals("Pending", result.getStatus());
+ assertEquals("AMP", result.getProjectFlavor());
+ assertEquals("/api/pub/v0/projects/PROJ01", result.getLocation());
+ }
+
+ @Test
+ void to_api_response_returns_null_when_input_is_null() {
+ ProjectResponse serviceResponse = null;
+
+ CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse);
+
+ assertNull(result);
+ }
+
+ @Test
+ void to_api_response_maps_with_null_status() {
+ ProjectResponse serviceResponse = ProjectResponse.builder()
+ .projectKey("PROJ02")
+ .status(null)
+ .build();
+
+ CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse);
+
+ assertNotNull(result);
+ assertEquals("PROJ02", result.getProjectKey());
+ assertNull(result.getStatus());
+ assertEquals("/api/pub/v0/projects/PROJ02", result.getLocation());
+ }
+
+ @Test
+ void to_api_response_does_not_set_location_when_project_key_is_null() {
+ ProjectResponse serviceResponse = ProjectResponse.builder()
+ .projectKey(null)
+ .status(Status.RUNNING)
+ .build();
+
+ CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse);
+
+ assertNotNull(result);
+ assertNull(result.getProjectKey());
+ assertEquals("Running", result.getStatus());
+ assertNull(result.getLocation());
+ }
+
+ @Test
+ void to_api_response_does_not_set_location_when_project_key_is_empty() {
+ ProjectResponse serviceResponse = ProjectResponse.builder()
+ .projectKey("")
+ .status(Status.FAILED)
+ .build();
+
+ CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse);
+
+ assertNotNull(result);
+ assertEquals("", result.getProjectKey());
+ assertEquals("Failed", result.getStatus());
+ assertNull(result.getLocation());
+ }
+
+ @Test
+ void to_api_response_maps_status_running_to_db_value() {
+ ProjectResponse serviceResponse = ProjectResponse.builder()
+ .projectKey("RUN01")
+ .status(Status.RUNNING)
+ .build();
+
+ CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse);
+
+ assertNotNull(result);
+ assertEquals("Running", result.getStatus());
+ }
+
+ @Test
+ void to_api_response_maps_status_failed_to_db_value() {
+ ProjectResponse serviceResponse = ProjectResponse.builder()
+ .projectKey("FAIL01")
+ .status(Status.FAILED)
+ .build();
+
+ CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse);
+
+ assertNotNull(result);
+ assertEquals("Failed", result.getStatus());
+ }
+
+ @Test
+ void to_api_response_maps_null_project_flavor() {
+ ProjectResponse serviceResponse = ProjectResponse.builder()
+ .projectKey("PROJ03")
+ .status(Status.PENDING)
+ .projectFlavor(null)
+ .build();
+
+ CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse);
+
+ assertNotNull(result);
+ assertEquals("PROJ03", result.getProjectKey());
+ assertNull(result.getProjectFlavor());
+ }
+
+ @Test
+ void to_api_response_sets_error_description_when_status_is_failed() {
+ ProjectResponse serviceResponse = ProjectResponse.builder()
+ .projectKey("FAIL01")
+ .status(Status.FAILED)
+ .build();
+
+ CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse);
+
+ assertNotNull(result);
+ assertEquals(
+ "There was an error when creating the project FAIL01.\n\n " +
+ "The error has been reported to our Support team as an incident. " +
+ "You will be informed about the incident via email.",
+ result.getErrorDescription()
+ );
+ }
+
+ @Test
+ void to_api_response_sets_null_error_description_when_status_is_not_failed() {
+ ProjectResponse serviceResponse = ProjectResponse.builder()
+ .projectKey("PROJ01")
+ .status(Status.RUNNING)
+ .build();
+
+ CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse);
+
+ assertNotNull(result);
+ assertNull(result.getErrorDescription());
+ }
+}
diff --git a/core/pom.xml b/core/pom.xml
index 6705b44..c3aa14b 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -41,6 +41,14 @@
spring-boot-starter-actuator
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
+
org.opendevstack.apiservice
@@ -119,6 +127,13 @@
${project.version}
+
+
+ org.opendevstack.apiservice
+ service-projects
+ ${project.version}
+
+
org.opendevstack.apiservice
diff --git a/core/src/main/java/org/opendevstack/apiservice/core/DevstackApiServiceApplication.java b/core/src/main/java/org/opendevstack/apiservice/core/DevstackApiServiceApplication.java
index bcb4866..f13e57a 100644
--- a/core/src/main/java/org/opendevstack/apiservice/core/DevstackApiServiceApplication.java
+++ b/core/src/main/java/org/opendevstack/apiservice/core/DevstackApiServiceApplication.java
@@ -2,10 +2,14 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication(scanBasePackages = { "org.opendevstack.apiservice" })
@EnableCaching
+@EntityScan(basePackages = "org.opendevstack.apiservice.persistence.entity")
+@EnableJpaRepositories(basePackages = "org.opendevstack.apiservice.persistence.repository")
public class DevstackApiServiceApplication {
public static void main(String[] args) {
diff --git a/core/src/main/java/org/opendevstack/apiservice/core/config/JacksonConfig.java b/core/src/main/java/org/opendevstack/apiservice/core/config/JacksonConfig.java
new file mode 100644
index 0000000..da25ff0
--- /dev/null
+++ b/core/src/main/java/org/opendevstack/apiservice/core/config/JacksonConfig.java
@@ -0,0 +1,21 @@
+package org.opendevstack.apiservice.core.config;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * Global Jackson configuration.
+ * Ensures that null properties are omitted from all JSON responses.
+ */
+@Configuration
+public class JacksonConfig {
+
+ @Bean
+ public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() {
+ return builder -> builder.serializationInclusion(JsonInclude.Include.NON_NULL);
+ }
+}
+
+
diff --git a/persistence/src/main/java/org/opendevstack/apiservice/persistence/entity/ProjectEntity.java b/persistence/src/main/java/org/opendevstack/apiservice/persistence/entity/ProjectEntity.java
index 635a1e7..f3299cb 100644
--- a/persistence/src/main/java/org/opendevstack/apiservice/persistence/entity/ProjectEntity.java
+++ b/persistence/src/main/java/org/opendevstack/apiservice/persistence/entity/ProjectEntity.java
@@ -78,7 +78,8 @@ public class ProjectEntity {
private String projectFlavor;
/**
- * Provisioning status. Known values: {@code Failed}, {@code Pending} (null = completed).
+ * Provisioning status. Known values: {@code Pending}, {@code Running}, {@code Failed}
+ * ({@code null} = completed successfully).
*/
@Column(name = "status", length = 50)
private String status;
diff --git a/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/ProjectRepository.java b/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/ProjectRepository.java
index 68f344c..2a5dc00 100644
--- a/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/ProjectRepository.java
+++ b/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/ProjectRepository.java
@@ -20,26 +20,11 @@
*/
@Repository
public interface ProjectRepository extends JpaRepository {
-
- /**
- * Finds a project by its unique business key regardless of soft-delete status. Use
- * this when you explicitly need to handle deleted projects (e.g. restore flows).
- * @param projectKey the Atlassian-style project key
- * @return the matching project, or {@link Optional#empty()} if it has never existed
- */
- Optional findByProjectKey(String projectKey);
-
- /**
- * Returns all active (non-deleted) projects.
- * @return list of active projects; empty list if none exist
- */
+
+ Optional findByProjectKeyIgnoreCase(String projectKey);
+
List findByDeletedFalse();
-
- /**
- * Checks whether an active project with the given key already exists.
- * @param projectKey the project key to look up
- * @return {@code true} if an active project with this key exists
- */
- boolean existsByProjectKey(String projectKey);
+
+ boolean existsByProjectKeyIgnoreCase(String projectKey);
}
diff --git a/persistence/src/test/java/org/opendevstack/apiservice/persistence/PersistenceTestApplication.java b/persistence/src/test/java/org/opendevstack/apiservice/persistence/PersistenceTestApplication.java
index 164c08e..0360a18 100644
--- a/persistence/src/test/java/org/opendevstack/apiservice/persistence/PersistenceTestApplication.java
+++ b/persistence/src/test/java/org/opendevstack/apiservice/persistence/PersistenceTestApplication.java
@@ -10,8 +10,6 @@
*/
@SpringBootConfiguration
@EnableAutoConfiguration
-@EntityScan(basePackages = "org.opendevstack.apiservice.persistence.entity")
-@EnableJpaRepositories(basePackages = "org.opendevstack.apiservice.persistence.repository")
public class PersistenceTestApplication {
}
\ No newline at end of file
diff --git a/persistence/src/test/java/org/opendevstack/apiservice/persistence/integration/ProjectRepositoryIntegrationTest.java b/persistence/src/test/java/org/opendevstack/apiservice/persistence/integration/ProjectRepositoryIntegrationTest.java
index be517a6..b944e73 100644
--- a/persistence/src/test/java/org/opendevstack/apiservice/persistence/integration/ProjectRepositoryIntegrationTest.java
+++ b/persistence/src/test/java/org/opendevstack/apiservice/persistence/integration/ProjectRepositoryIntegrationTest.java
@@ -113,19 +113,17 @@ void tearDown() {
}
private void cleanupTestData() {
- TEST_KEYS.forEach(key -> projectRepository.findByProjectKey(key).ifPresent(projectRepository::delete));
+ TEST_KEYS.forEach(key -> projectRepository.findByProjectKeyIgnoreCase(key).ifPresent(projectRepository::delete));
}
- // ── findByProjectKey ──────────────────────────────────────────────────────
-
@Nested
- @DisplayName("findByProjectKey")
- class FindByProjectKey {
+ @DisplayName("findByProjectKeyIgnoreCase")
+ class findByProjectKeyIgnoreCase {
@Test
@DisplayName("returns active project by its key")
void returnsActiveProjectByKey() {
- Optional result = projectRepository.findByProjectKey("IT-ACTIVE-01");
+ Optional result = projectRepository.findByProjectKeyIgnoreCase("IT-ACTIVE-01");
assertThat(result).isPresent();
assertThat(result.get().getProjectKey()).isEqualTo("IT-ACTIVE-01");
@@ -135,7 +133,7 @@ void returnsActiveProjectByKey() {
@Test
@DisplayName("returns soft-deleted project by its key")
void returnsDeletedProjectByKey() {
- Optional result = projectRepository.findByProjectKey("IT-DELETED-01");
+ Optional result = projectRepository.findByProjectKeyIgnoreCase("IT-DELETED-01");
assertThat(result).isPresent();
assertThat(result.get().getProjectKey()).isEqualTo("IT-DELETED-01");
@@ -145,7 +143,7 @@ void returnsDeletedProjectByKey() {
@Test
@DisplayName("returns empty Optional for a key that has never existed")
void returnsEmptyForUnknownKey() {
- Optional result = projectRepository.findByProjectKey("NONEXISTENTKEY");
+ Optional result = projectRepository.findByProjectKeyIgnoreCase("NONEXISTENTKEY");
assertThat(result).isEmpty();
}
@@ -153,7 +151,7 @@ void returnsEmptyForUnknownKey() {
@Test
@DisplayName("returned entity contains all persisted fields")
void returnedEntityContainsAllFields() {
- Optional result = projectRepository.findByProjectKey("IT-DELETED-01");
+ Optional result = projectRepository.findByProjectKeyIgnoreCase("IT-DELETED-01");
assertThat(result).isPresent();
ProjectEntity entity = result.get();
@@ -164,8 +162,6 @@ void returnedEntityContainsAllFields() {
}
}
- // ── findByDeletedFalse ────────────────────────────────────────────────────
-
@Nested
@DisplayName("findByDeletedFalse")
class FindByDeletedFalse {
@@ -212,24 +208,22 @@ class ExistsByProjectKey {
@Test
@DisplayName("returns true for an existing active project key")
void returnsTrueForActiveKey() {
- assertThat(projectRepository.existsByProjectKey("IT-ACTIVE-01")).isTrue();
+ assertThat(projectRepository.existsByProjectKeyIgnoreCase("IT-ACTIVE-01")).isTrue();
}
@Test
@DisplayName("returns true for a soft-deleted project key")
void returnsTrueForDeletedKey() {
- assertThat(projectRepository.existsByProjectKey("IT-DELETED-01")).isTrue();
+ assertThat(projectRepository.existsByProjectKeyIgnoreCase("IT-DELETED-01")).isTrue();
}
@Test
@DisplayName("returns false for a key that has never existed")
void returnsFalseForNonExistentKey() {
- assertThat(projectRepository.existsByProjectKey("NONEXISTENTKEY")).isFalse();
+ assertThat(projectRepository.existsByProjectKeyIgnoreCase("NONEXISTENTKEY")).isFalse();
}
}
-
- // ── Persistence lifecycle ─────────────────────────────────────────────────
-
+
@Nested
@DisplayName("Persistence lifecycle")
class PersistenceLifecycle {
diff --git a/persistence/src/test/java/org/opendevstack/apiservice/persistence/repository/ProjectRepositoryTest.java b/persistence/src/test/java/org/opendevstack/apiservice/persistence/repository/ProjectRepositoryTest.java
index e067117..ca30d90 100644
--- a/persistence/src/test/java/org/opendevstack/apiservice/persistence/repository/ProjectRepositoryTest.java
+++ b/persistence/src/test/java/org/opendevstack/apiservice/persistence/repository/ProjectRepositoryTest.java
@@ -123,24 +123,22 @@ void save_duplicateProjectKey_throwsException() {
}
}
-
- // ── findByProjectKey ──────────────────────────────────────────────────────
-
+
@Nested
- @DisplayName("findByProjectKey")
- class FindByProjectKey {
+ @DisplayName("findByProjectKeyIgnoreCase")
+ class findByProjectKeyIgnoreCase {
@Test
@DisplayName("returns project regardless of deleted flag")
void returnsProjectRegardlessOfDeletedFlag() {
- assertThat(repository.findByProjectKey("ACTIVE-01")).isPresent();
- assertThat(repository.findByProjectKey("DELETED-01")).isPresent();
+ assertThat(repository.findByProjectKeyIgnoreCase("ACTIVE-01")).isPresent();
+ assertThat(repository.findByProjectKeyIgnoreCase("DELETED-01")).isPresent();
}
@Test
@DisplayName("returns empty for unknown key")
void returnsEmptyForUnknownKey() {
- assertThat(repository.findByProjectKey("UNKNOWN")).isEmpty();
+ assertThat(repository.findByProjectKeyIgnoreCase("UNKNOWN")).isEmpty();
}
}
diff --git a/persistence/src/test/resources/application.properties b/persistence/src/test/resources/application.properties
new file mode 100644
index 0000000..bee83c5
--- /dev/null
+++ b/persistence/src/test/resources/application.properties
@@ -0,0 +1,18 @@
+# ── HikariCP tuning for Testcontainers ─────────────────────────────────────────
+#
+# When the @Container PostgreSQL instance stops (after the test class finishes),
+# HikariCP's background keepalive thread tries to validate open connections and
+# logs "Failed to validate connection" warnings. These settings eliminate those
+# warnings and make the pool give up quickly on unreachable connections during
+# teardown — they have no effect on test correctness.
+
+# Disable background keepalive probes entirely (default: 0 = disabled already,
+# but some Spring Boot auto-config versions set it higher).
+spring.datasource.hikari.keepalive-time=0
+
+# Reduce how long HikariCP waits to acquire a connection (default: 30 000 ms).
+# During teardown this prevents the pool from blocking on a dead container.
+spring.datasource.hikari.connection-timeout=3000
+
+# Reduce how long HikariCP waits to validate a connection (default: 5 000 ms).
+spring.datasource.hikari.validation-timeout=1000
diff --git a/pom.xml b/pom.xml
index c825aa0..6076af2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -36,6 +36,7 @@
0.0.47
0.2.7
3.6.1
+ 1.6.3
diff --git a/service-projects/pom.xml b/service-projects/pom.xml
index cd1a931..2a82727 100644
--- a/service-projects/pom.xml
+++ b/service-projects/pom.xml
@@ -35,6 +35,18 @@
${jackson-databind-nullable.version}
+
+ org.mapstruct
+ mapstruct
+ ${mapstruct.version}
+
+
+
+ org.opendevstack.apiservice
+ persistence
+ ${project.version}
+
+
org.opendevstack.apiservice
external-service-api
@@ -71,5 +83,28 @@
test
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+
+ org.projectlombok
+ lombok
+ ${lombok.version}
+
+
+ org.mapstruct
+ mapstruct-processor
+ ${mapstruct.version}
+
+
+
+
+
+
diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/ProjectResponseMapper.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/ProjectResponseMapper.java
new file mode 100644
index 0000000..18a6491
--- /dev/null
+++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/ProjectResponseMapper.java
@@ -0,0 +1,29 @@
+package org.opendevstack.apiservice.serviceproject.mapper;
+
+import java.util.Arrays;
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.Named;
+import org.opendevstack.apiservice.persistence.entity.ProjectEntity;
+import org.opendevstack.apiservice.serviceproject.model.ProjectResponse;
+import org.opendevstack.apiservice.serviceproject.model.Status;
+
+@Mapper(componentModel = "spring")
+public interface ProjectResponseMapper {
+
+ @Mapping(source = "status", target = "status", qualifiedByName = "mapStatus")
+ ProjectResponse toCreateProjectResponse(ProjectEntity entity);
+
+ @Named("mapStatus")
+ default Status mapStatus(String value) {
+ if (value == null) {
+ return null;
+ }
+
+ return Arrays.stream(Status.values())
+ .filter(s -> s.getDbValue().equalsIgnoreCase(value))
+ .findFirst()
+ .orElseThrow(() -> new IllegalArgumentException(
+ "Unknown Status db value: '" + value + "'"));
+ }
+}
diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectRequest.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectRequest.java
similarity index 90%
rename from service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectRequest.java
rename to service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectRequest.java
index 22122b6..186ffaa 100644
--- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectRequest.java
+++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectRequest.java
@@ -7,7 +7,7 @@
@Data
@NoArgsConstructor
@AllArgsConstructor
-public class CreateProjectRequest {
+public class ProjectRequest {
private String projectKey;
diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectResponse.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectResponse.java
similarity index 57%
rename from service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectResponse.java
rename to service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectResponse.java
index 1dcd607..40ee62a 100644
--- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectResponse.java
+++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectResponse.java
@@ -9,17 +9,11 @@
@NoArgsConstructor
@AllArgsConstructor
@Builder
-public class CreateProjectResponse {
+public class ProjectResponse {
private String projectKey;
- private String status;
+ private Status status;
- private String message;
-
- private String error;
-
- private String errorKey;
-
- private String errorDescription;
+ private String projectFlavor;
}
diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/Status.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/Status.java
new file mode 100644
index 0000000..960efc6
--- /dev/null
+++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/Status.java
@@ -0,0 +1,15 @@
+package org.opendevstack.apiservice.serviceproject.model;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+@Getter
+@RequiredArgsConstructor
+public enum Status {
+
+ PENDING("Pending"),
+ RUNNING("Running"),
+ FAILED("Failed");
+
+ private final String dbValue;
+}
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 63f2d66..c2abcc6 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
@@ -1,12 +1,12 @@
package org.opendevstack.apiservice.serviceproject.service;
-import org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest;
-import org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse;
+import org.opendevstack.apiservice.serviceproject.model.ProjectRequest;
+import org.opendevstack.apiservice.serviceproject.model.ProjectResponse;
public interface ProjectService {
- CreateProjectResponse createProject(CreateProjectRequest request);
+ ProjectResponse createProject(ProjectRequest request);
- CreateProjectResponse getProject(String projectKey);
+ ProjectResponse getProject(String projectKey);
}
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 3e8caab..1d8d7b6 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
@@ -5,12 +5,17 @@
import org.opendevstack.apiservice.externalservice.bitbucket.service.BitbucketService;
import org.opendevstack.apiservice.externalservice.jira.service.JiraService;
import org.opendevstack.apiservice.externalservice.ocp.service.OpenshiftService;
-import org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest;
-import org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse;
+import org.opendevstack.apiservice.persistence.entity.ProjectEntity;
+import org.opendevstack.apiservice.persistence.repository.ProjectRepository;
+import org.opendevstack.apiservice.serviceproject.mapper.ProjectResponseMapper;
+import org.opendevstack.apiservice.serviceproject.model.ProjectRequest;
+import org.opendevstack.apiservice.serviceproject.model.ProjectResponse;
import org.opendevstack.apiservice.serviceproject.service.GenerateProjectKeyService;
import org.opendevstack.apiservice.serviceproject.service.ProjectService;
import org.springframework.stereotype.Service;
+import java.util.Optional;
+
@Service
@Slf4j
@AllArgsConstructor
@@ -23,15 +28,25 @@ public class ProjectServiceImpl implements ProjectService {
private final JiraService jiraService;
private final GenerateProjectKeyService generateProjectKeyService;
+
+ private final ProjectRepository projectRepository;
+
+ private final ProjectResponseMapper projectResponseMapper;
@Override
- public CreateProjectResponse createProject(CreateProjectRequest request) {
- return CreateProjectResponse.builder().build();
+ public ProjectResponse createProject(ProjectRequest request) {
+ return ProjectResponse.builder().build();
}
@Override
- public CreateProjectResponse getProject(String projectKey) {
- return CreateProjectResponse.builder().build();
+ public ProjectResponse getProject(String projectKey) {
+ Optional project = projectRepository.findByProjectKeyIgnoreCase(projectKey);
+
+ if (project.isPresent()) {
+ return projectResponseMapper.toCreateProjectResponse(project.get());
+ }
+
+ return null;
}
}
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
new file mode 100644
index 0000000..827ba41
--- /dev/null
+++ b/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java
@@ -0,0 +1,192 @@
+package org.opendevstack.apiservice.serviceproject.service.impl;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.opendevstack.apiservice.externalservice.bitbucket.service.BitbucketService;
+import org.opendevstack.apiservice.externalservice.jira.service.JiraService;
+import org.opendevstack.apiservice.externalservice.ocp.service.OpenshiftService;
+import org.opendevstack.apiservice.persistence.entity.ProjectEntity;
+import org.opendevstack.apiservice.persistence.repository.ProjectRepository;
+import org.opendevstack.apiservice.serviceproject.mapper.ProjectResponseMapper;
+import org.opendevstack.apiservice.serviceproject.model.ProjectRequest;
+import org.opendevstack.apiservice.serviceproject.model.ProjectResponse;
+import org.opendevstack.apiservice.serviceproject.model.Status;
+import org.opendevstack.apiservice.serviceproject.service.GenerateProjectKeyService;
+
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class ProjectServiceImplTest {
+
+ @Mock
+ private OpenshiftService openshiftService;
+
+ @Mock
+ private BitbucketService bitbucketService;
+
+ @Mock
+ private JiraService jiraService;
+
+ @Mock
+ private GenerateProjectKeyService generateProjectKeyService;
+
+ @Mock
+ private ProjectRepository projectRepository;
+
+ @Mock
+ private ProjectResponseMapper projectResponseMapper;
+
+ private ProjectServiceImpl projectService;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ projectService = new ProjectServiceImpl(
+ openshiftService,
+ bitbucketService,
+ jiraService,
+ generateProjectKeyService,
+ projectRepository,
+ projectResponseMapper
+ );
+ }
+
+ @Test
+ void get_project_returns_response_when_project_exists() {
+
+ String projectKey = "MY-PROJECT";
+ UUID projectId = UUID.randomUUID();
+
+ ProjectEntity projectEntity = ProjectEntity.builder()
+ .id(projectId)
+ .projectKey(projectKey)
+ .projectName("My Project")
+ .description("Test project")
+ .configurationItem("CI-123")
+ .location("eu")
+ .projectFlavor("AMP")
+ .status("Completed")
+ .deleted(false)
+ .ldapGroupManager("cn=my-project-manager,ou=groups,dc=example,dc=com")
+ .ldapGroupTeam("cn=my-project-team,ou=groups,dc=example,dc=com")
+ .build();
+
+ ProjectResponse expectedResponse = ProjectResponse.builder()
+ .projectKey(projectKey)
+ .status(Status.RUNNING)
+ .build();
+
+ when(projectRepository.findByProjectKeyIgnoreCase(projectKey)).thenReturn(Optional.of(projectEntity));
+ when(projectResponseMapper.toCreateProjectResponse(projectEntity)).thenReturn(expectedResponse);
+
+
+ ProjectResponse result = projectService.getProject(projectKey);
+
+
+ assertNotNull(result);
+ assertEquals(projectKey, result.getProjectKey());
+ assertEquals(Status.RUNNING, result.getStatus());
+ verify(projectRepository).findByProjectKeyIgnoreCase(projectKey);
+ verify(projectResponseMapper).toCreateProjectResponse(projectEntity);
+ }
+
+ @Test
+ void get_project_returns_null_when_project_does_not_exist() {
+
+ String projectKey = "NON-EXISTING";
+
+ when(projectRepository.findByProjectKeyIgnoreCase(projectKey)).thenReturn(Optional.empty());
+
+
+ ProjectResponse result = projectService.getProject(projectKey);
+
+
+ assertNull(result);
+ verify(projectRepository).findByProjectKeyIgnoreCase(projectKey);
+ verify(projectResponseMapper, never()).toCreateProjectResponse(any());
+ }
+
+ @Test
+ void create_project_returns_empty_response() {
+
+ ProjectRequest request = new ProjectRequest();
+ request.setProjectKey("NEW-PROJECT");
+ request.setProjectKeyPattern("NEW%06d");
+ request.setProjectName("New Project");
+ request.setProjectDescription("New test project");
+
+
+ ProjectResponse result = projectService.createProject(request);
+
+
+ assertNotNull(result);
+ assertNull(result.getProjectKey());
+ }
+
+ @Test
+ void get_project_propagates_repository_exception() {
+
+ String projectKey = "ERROR-PROJECT";
+
+ when(projectRepository.findByProjectKeyIgnoreCase(projectKey))
+ .thenThrow(new RuntimeException("Database connection error"));
+
+ assertThrows(RuntimeException.class, () -> projectService.getProject(projectKey));
+ verify(projectRepository).findByProjectKeyIgnoreCase(projectKey);
+ }
+
+ @Test
+ void get_project_returns_null_when_project_key_is_null() {
+
+ when(projectRepository.findByProjectKeyIgnoreCase(null)).thenReturn(Optional.empty());
+
+
+ ProjectResponse result = projectService.getProject(null);
+
+
+ assertNull(result);
+ verify(projectRepository).findByProjectKeyIgnoreCase(null);
+ }
+
+ @Test
+ void get_project_returns_response_for_soft_deleted_project() {
+
+ String projectKey = "DELETED-PROJECT";
+
+ ProjectEntity deletedEntity = ProjectEntity.builder()
+ .id(UUID.randomUUID())
+ .projectKey(projectKey)
+ .projectName("Deleted Project")
+ .configurationItem("CI-456")
+ .location("eu")
+ .deleted(true)
+ .build();
+
+ ProjectResponse expectedResponse = ProjectResponse.builder()
+ .projectKey(projectKey)
+ .status(Status.RUNNING)
+ .build();
+
+ when(projectRepository.findByProjectKeyIgnoreCase(projectKey)).thenReturn(Optional.of(deletedEntity));
+ when(projectResponseMapper.toCreateProjectResponse(deletedEntity)).thenReturn(expectedResponse);
+
+
+ ProjectResponse result = projectService.getProject(projectKey);
+
+
+ assertNotNull(result);
+ assertEquals(projectKey, result.getProjectKey());
+ verify(projectRepository).findByProjectKeyIgnoreCase(projectKey);
+ }
+}
\ No newline at end of file