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/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerIntegrationTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerIntegrationTest.java
new file mode 100644
index 0000000..2ee32d5
--- /dev/null
+++ b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerIntegrationTest.java
@@ -0,0 +1,87 @@
+package org.opendevstack.apiservice.project.controller;
+
+import org.junit.jupiter.api.Test;
+import org.opendevstack.apiservice.project.facade.ProjectsFacade;
+import org.opendevstack.apiservice.project.model.CreateProjectRequest;
+import org.opendevstack.apiservice.project.model.CreateProjectResponse;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.SpringBootConfiguration;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
+import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.springframework.test.context.bean.override.mockito.MockitoBean;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@SpringBootTest(classes = ProjectControllerIntegrationTest.TestConfig.class)
+@AutoConfigureMockMvc(addFilters = false)
+class ProjectControllerIntegrationTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @MockitoBean
+ private ProjectsFacade facade;
+
+ @Test
+ void createProject_withProvidedProjectKey_returnsInitiatedResponse() throws Exception {
+ CreateProjectResponse serviceResponse =
+ new CreateProjectResponse();
+ serviceResponse.setProjectKey("PROJ01");
+ serviceResponse.setStatus("Initiated");
+ when(facade.createProject(any(CreateProjectRequest.class)))
+ .thenReturn(serviceResponse);
+
+ String payload = """
+ {
+ \"projectKey\": \"PROJ01\",
+ \"projectName\": \"My Project\",
+ \"projectDescription\": \"desc\"
+ }
+ """;
+
+ mockMvc.perform(post("/api/pub/v0/projects")
+ .contentType("application/json")
+ .content(payload == null ? "" : payload))
+ .andExpect(status().isOk())
+ .andExpect(jsonPath("$.projectKey").value("PROJ01"))
+ .andExpect(jsonPath("$.status").value("Initiated"))
+ .andExpect(jsonPath("$.error").doesNotExist())
+ .andExpect(jsonPath("$.errorKey").doesNotExist())
+ .andExpect(jsonPath("$.errorDescription").doesNotExist());
+ }
+
+ @Test
+ void getProject_whenNotFound_returns404() throws Exception {
+ when(facade.getProject("UNKNOWN")).thenReturn(null);
+
+ mockMvc.perform(get("/api/pub/v0/projects/UNKNOWN"))
+ .andExpect(status().isNotFound())
+ .andExpect(jsonPath("$.error").value("NOT_FOUND"))
+ .andExpect(jsonPath("$.errorKey").value("PROJECT_NOT_FOUND"))
+ .andExpect(jsonPath("$.message").value("Project with key 'UNKNOWN' not found"))
+ .andExpect(jsonPath("$.projectKey").doesNotExist())
+ .andExpect(jsonPath("$.status").doesNotExist())
+ .andExpect(jsonPath("$.errorDescription").doesNotExist());
+ }
+
+ @SpringBootConfiguration
+ @EnableAutoConfiguration(exclude = {
+ DataSourceAutoConfiguration.class,
+ DataSourceTransactionManagerAutoConfiguration.class,
+ HibernateJpaAutoConfiguration.class
+ })
+ @Import({ProjectController.class})
+ static class TestConfig {
+ }
+}
\ No newline at end of file
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..9e73084 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
@@ -68,6 +71,9 @@ void createProject_whenProjectCreationException_thenReturnConflict() throws Exce
assertThat(result.getBody().getError()).isEqualTo("CONFLICT");
assertThat(result.getBody().getErrorKey()).isEqualTo("PROJECT_ALREADY_EXISTS");
assertThat(result.getBody().getMessage()).contains("already exists");
+ assertThat(result.getBody().getProjectKey()).isNull();
+ assertThat(result.getBody().getStatus()).isNull();
+ assertThat(result.getBody().getErrorDescription()).isNull();
}
@Test
@@ -84,6 +90,9 @@ void createProject_whenProjectKeyGenerationException_thenReturnInternalServerErr
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");
}
@@ -113,6 +124,9 @@ void getProject_whenNotFound_thenReturnNotFound() throws Exception {
assertThat(result.getBody().getError()).isEqualTo("NOT_FOUND");
assertThat(result.getBody().getErrorKey()).isEqualTo("PROJECT_NOT_FOUND");
assertThat(result.getBody().getMessage()).contains("UNKNOWN");
+ assertThat(result.getBody().getProjectKey()).isNull();
+ assertThat(result.getBody().getStatus()).isNull();
+ assertThat(result.getBody().getErrorDescription()).isNull();
}
@Test
@@ -127,6 +141,9 @@ void getProject_whenServiceThrows_thenReturnInternalServerError() throws Excepti
assertThat(result.getBody().getError()).isEqualTo("INTERNAL_ERROR");
assertThat(result.getBody().getErrorKey()).isEqualTo("INTERNAL_ERROR");
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/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/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/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/CreateProjectResponseMapper.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/CreateProjectResponseMapper.java
new file mode 100644
index 0000000..7f187fd
--- /dev/null
+++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/CreateProjectResponseMapper.java
@@ -0,0 +1,16 @@
+package org.opendevstack.apiservice.serviceproject.mapper;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.opendevstack.apiservice.persistence.entity.ProjectEntity;
+import org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse;
+
+@Mapper(componentModel = "spring")
+public interface CreateProjectResponseMapper {
+
+ @Mapping(target = "message", ignore = true)
+ @Mapping(target = "error", ignore = true)
+ @Mapping(target = "errorKey", ignore = true)
+ @Mapping(target = "errorDescription", ignore = true)
+ CreateProjectResponse toCreateProjectResponse(ProjectEntity entity);
+}
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..bee32a8 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.persistence.entity.ProjectEntity;
+import org.opendevstack.apiservice.persistence.repository.ProjectRepository;
+import org.opendevstack.apiservice.serviceproject.mapper.CreateProjectResponseMapper;
import org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest;
import org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse;
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,6 +28,10 @@ public class ProjectServiceImpl implements ProjectService {
private final JiraService jiraService;
private final GenerateProjectKeyService generateProjectKeyService;
+
+ private final ProjectRepository projectRepository;
+
+ private final CreateProjectResponseMapper createProjectResponseMapper;
@Override
public CreateProjectResponse createProject(CreateProjectRequest request) {
@@ -31,7 +40,13 @@ public CreateProjectResponse createProject(CreateProjectRequest request) {
@Override
public CreateProjectResponse getProject(String projectKey) {
- return CreateProjectResponse.builder().build();
+ Optional project = projectRepository.findByProjectKey(projectKey);
+
+ if (project.isPresent()) {
+ return createProjectResponseMapper.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..e9db6ae
--- /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.CreateProjectResponseMapper;
+import org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest;
+import org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse;
+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 CreateProjectResponseMapper createProjectResponseMapper;
+
+ private ProjectServiceImpl projectService;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ projectService = new ProjectServiceImpl(
+ openshiftService,
+ bitbucketService,
+ jiraService,
+ generateProjectKeyService,
+ projectRepository,
+ createProjectResponseMapper
+ );
+ }
+
+ @Test
+ void get_project_returns_response_when_project_exists() {
+ // GIVEN
+ 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();
+
+ CreateProjectResponse expectedResponse = CreateProjectResponse.builder()
+ .projectKey(projectKey)
+ .status("Completed")
+ .build();
+
+ when(projectRepository.findByProjectKey(projectKey)).thenReturn(Optional.of(projectEntity));
+ when(createProjectResponseMapper.toCreateProjectResponse(projectEntity)).thenReturn(expectedResponse);
+
+ // WHEN
+ CreateProjectResponse result = projectService.getProject(projectKey);
+
+ // THEN
+ assertNotNull(result);
+ assertEquals(projectKey, result.getProjectKey());
+ assertEquals("Completed", result.getStatus());
+ verify(projectRepository).findByProjectKey(projectKey);
+ verify(createProjectResponseMapper).toCreateProjectResponse(projectEntity);
+ }
+
+ @Test
+ void get_project_returns_null_when_project_does_not_exist() {
+ // GIVEN
+ String projectKey = "NON-EXISTING";
+
+ when(projectRepository.findByProjectKey(projectKey)).thenReturn(Optional.empty());
+
+ // WHEN
+ CreateProjectResponse result = projectService.getProject(projectKey);
+
+ // THEN
+ assertNull(result);
+ verify(projectRepository).findByProjectKey(projectKey);
+ verify(createProjectResponseMapper, never()).toCreateProjectResponse(any());
+ }
+
+ @Test
+ void create_project_returns_empty_response() {
+ // GIVEN
+ CreateProjectRequest request = new CreateProjectRequest();
+ request.setProjectKey("NEW-PROJECT");
+ request.setProjectKeyPattern("NEW%06d");
+ request.setProjectName("New Project");
+ request.setProjectDescription("New test project");
+
+ // WHEN
+ CreateProjectResponse result = projectService.createProject(request);
+
+ // THEN
+ assertNotNull(result);
+ assertNull(result.getProjectKey());
+ }
+
+ @Test
+ void get_project_propagates_repository_exception() {
+ // GIVEN
+ String projectKey = "ERROR-PROJECT";
+
+ when(projectRepository.findByProjectKey(projectKey))
+ .thenThrow(new RuntimeException("Database connection error"));
+
+ // WHEN / THEN
+ assertThrows(RuntimeException.class, () -> projectService.getProject(projectKey));
+ verify(projectRepository).findByProjectKey(projectKey);
+ }
+
+ @Test
+ void get_project_returns_null_when_project_key_is_null() {
+ // GIVEN
+ when(projectRepository.findByProjectKey(null)).thenReturn(Optional.empty());
+
+ // WHEN
+ CreateProjectResponse result = projectService.getProject(null);
+
+ // THEN
+ assertNull(result);
+ verify(projectRepository).findByProjectKey(null);
+ }
+
+ @Test
+ void get_project_returns_response_for_soft_deleted_project() {
+ // GIVEN
+ String projectKey = "DELETED-PROJECT";
+
+ ProjectEntity deletedEntity = ProjectEntity.builder()
+ .id(UUID.randomUUID())
+ .projectKey(projectKey)
+ .projectName("Deleted Project")
+ .configurationItem("CI-456")
+ .location("eu")
+ .deleted(true)
+ .build();
+
+ CreateProjectResponse expectedResponse = CreateProjectResponse.builder()
+ .projectKey(projectKey)
+ .status("Deleted")
+ .build();
+
+ when(projectRepository.findByProjectKey(projectKey)).thenReturn(Optional.of(deletedEntity));
+ when(createProjectResponseMapper.toCreateProjectResponse(deletedEntity)).thenReturn(expectedResponse);
+
+ // WHEN
+ CreateProjectResponse result = projectService.getProject(projectKey);
+
+ // THEN
+ assertNotNull(result);
+ assertEquals(projectKey, result.getProjectKey());
+ verify(projectRepository).findByProjectKey(projectKey);
+ }
+}
\ No newline at end of file