From 2a626d1e260fd0911236dffdf901dff2a54f5ed8 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Thu, 5 Mar 2026 12:04:01 +0100 Subject: [PATCH 01/14] Add project service module with key generation and existence checks --- external-service-projects/pom.xml | 69 +++++++++ .../ProjectKeyGenerationException.java | 13 ++ .../model/CreateProjectRequest.java | 17 +++ .../model/CreateProjectResponse.java | 21 +++ .../service/GenerateProjectKeyService.java | 11 ++ .../service/ProjectService.java | 12 ++ .../impl/GenerateProjectKeyServiceImpl.java | 144 ++++++++++++++++++ .../service/impl/ProjectServiceImpl.java | 48 ++++++ .../GenerateProjectKeyServiceImplTest.java | 92 +++++++++++ .../service/impl/ProjectServiceImplTest.java | 34 +++++ 10 files changed, 461 insertions(+) create mode 100644 external-service-projects/pom.xml create mode 100644 external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/exception/ProjectKeyGenerationException.java create mode 100644 external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectRequest.java create mode 100644 external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectResponse.java create mode 100644 external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/GenerateProjectKeyService.java create mode 100644 external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java create mode 100644 external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImpl.java create mode 100644 external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java create mode 100644 external-service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImplTest.java create mode 100644 external-service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java diff --git a/external-service-projects/pom.xml b/external-service-projects/pom.xml new file mode 100644 index 0000000..c6d622e --- /dev/null +++ b/external-service-projects/pom.xml @@ -0,0 +1,69 @@ + + 4.0.0 + + + org.opendevstack.apiservice + devstack-api-service + 0.0.2 + + + external-service-projects + External Service Projects + Service module for project operations: key generation and project existence checks + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-validation + + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + + + + org.openapitools + jackson-databind-nullable + ${jackson-databind-nullable.version} + + + + org.opendevstack.apiservice + external-service-bitbucket + ${project.version} + + + + org.opendevstack.apiservice + external-service-jira + ${project.version} + + + + org.opendevstack.apiservice + external-service-ocp + ${project.version} + + + + org.projectlombok + lombok + provided + + + + org.springframework.boot + spring-boot-starter-test + test + + + + diff --git a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/exception/ProjectKeyGenerationException.java b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/exception/ProjectKeyGenerationException.java new file mode 100644 index 0000000..dad4ffa --- /dev/null +++ b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/exception/ProjectKeyGenerationException.java @@ -0,0 +1,13 @@ +package org.opendevstack.apiservice.serviceproject.exception; + +public class ProjectKeyGenerationException extends Exception { + + public ProjectKeyGenerationException(String message) { + super(message); + } + + public ProjectKeyGenerationException(String message, Throwable cause) { + super(message, cause); + } +} + diff --git a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectRequest.java b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectRequest.java new file mode 100644 index 0000000..6127031 --- /dev/null +++ b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectRequest.java @@ -0,0 +1,17 @@ +package org.opendevstack.apiservice.serviceproject.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class CreateProjectRequest { + + private String projectKey; + + private String projectKeyPattern; + + private String projectName; + + private String projectDescription; +} diff --git a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectResponse.java b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectResponse.java new file mode 100644 index 0000000..0775c9b --- /dev/null +++ b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectResponse.java @@ -0,0 +1,21 @@ +package org.opendevstack.apiservice.serviceproject.model; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class CreateProjectResponse { + + private String projectKey; + + private String status; + + private String message; + + private String error; + + private String errorKey; + + private String errorDescription; +} diff --git a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/GenerateProjectKeyService.java b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/GenerateProjectKeyService.java new file mode 100644 index 0000000..33ffda0 --- /dev/null +++ b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/GenerateProjectKeyService.java @@ -0,0 +1,11 @@ +package org.opendevstack.apiservice.serviceproject.service; + +import org.opendevstack.apiservice.serviceproject.exception.ProjectKeyGenerationException; + +public interface GenerateProjectKeyService { + + String DEFAULT_PROJECT_KEY_PATTERN = "SS%06d"; + + String generateProjectKey(String projectKeyPattern) throws ProjectKeyGenerationException; +} + diff --git a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java new file mode 100644 index 0000000..0c59b5e --- /dev/null +++ b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java @@ -0,0 +1,12 @@ +package org.opendevstack.apiservice.serviceproject.service; + +import org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest; +import org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse; + +public interface ProjectService { + + CreateProjectResponse createProject(CreateProjectRequest request); + + CreateProjectResponse getProject(String projectKey); +} + diff --git a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImpl.java b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImpl.java new file mode 100644 index 0000000..c70b85d --- /dev/null +++ b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImpl.java @@ -0,0 +1,144 @@ +package org.opendevstack.apiservice.serviceproject.service.impl; + +import lombok.extern.slf4j.Slf4j; +import org.opendevstack.apiservice.externalservice.bitbucket.exception.BitbucketException; +import org.opendevstack.apiservice.externalservice.bitbucket.service.BitbucketService; +import org.opendevstack.apiservice.externalservice.jira.exception.JiraException; +import org.opendevstack.apiservice.externalservice.jira.service.JiraService; +import org.opendevstack.apiservice.externalservice.ocp.exception.OpenshiftException; +import org.opendevstack.apiservice.externalservice.ocp.service.OpenshiftService; +import org.opendevstack.apiservice.serviceproject.exception.ProjectKeyGenerationException; +import org.opendevstack.apiservice.serviceproject.service.GenerateProjectKeyService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Comparator; +import java.util.Random; +import java.util.Set; + +@Service +@Slf4j +public class GenerateProjectKeyServiceImpl implements GenerateProjectKeyService { + + private static final int MAX_RETRIES = 10; + + private final OpenshiftService openshiftService; + + private final BitbucketService bitbucketService; + + private final JiraService jiraService; + + private final Random random; + + @Autowired + public GenerateProjectKeyServiceImpl(BitbucketService bitbucketService, JiraService jiraService, + OpenshiftService openshiftService) { + this(bitbucketService, jiraService, openshiftService, new Random()); + } + + GenerateProjectKeyServiceImpl(BitbucketService bitbucketService, JiraService jiraService, + OpenshiftService openshiftService, Random random) { + this.bitbucketService = bitbucketService; + this.jiraService = jiraService; + this.openshiftService = openshiftService; + this.random = random; + } + + @Override + public String generateProjectKey(String projectKeyPattern) throws ProjectKeyGenerationException { + String pattern = resolveProjectKeyPattern(projectKeyPattern); + + for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { + int randomNumber = random.nextInt(1_000_000); + String projectKey = String.format(pattern, randomNumber); + + if (!isProjectFound(projectKey)) { + log.debug("Generated unique project key '{}' on attempt {}", projectKey, attempt); + return projectKey; + } + + log.debug("Project key '{}' already exists (attempt {}/{})", projectKey, attempt, MAX_RETRIES); + } + + throw new ProjectKeyGenerationException( + String.format("Failed to generate unique project key after %d retries", MAX_RETRIES)); + } + + private String resolveProjectKeyPattern(String projectKeyPattern) { + if (projectKeyPattern == null || projectKeyPattern.isBlank()) { + return DEFAULT_PROJECT_KEY_PATTERN; + } + return projectKeyPattern; + } + + private boolean isProjectFound(String projectKey) throws ProjectKeyGenerationException { + try { + if (existsInAnyBitbucketInstance(projectKey)) { + return true; + } + + if (existsInAnyJiraInstance(projectKey)) { + return true; + } + + if (existsInAnyOpenshift(projectKey)) { + return true; + } + + return false; + } catch (BitbucketException e) { + throw new ProjectKeyGenerationException( + String.format("Failed to check project '%s' in Bitbucket", projectKey), e); + } catch (JiraException e) { + throw new ProjectKeyGenerationException( + String.format("Failed to check project '%s' in Jira", projectKey), e); + } catch (OpenshiftException e) { + throw new ProjectKeyGenerationException( + String.format("Failed to check project '%s' in Openshift", projectKey), e); + } + } + + private boolean existsInAnyBitbucketInstance(String projectKey) throws BitbucketException { + Set instances = bitbucketService.getAvailableInstances(); + + for (String instanceName : instances.stream().sorted(Comparator.naturalOrder()).toList()) { + if (bitbucketService.projectExists(instanceName, projectKey)) { + return true; + } + } + + return false; + } + + private boolean existsInAnyJiraInstance(String projectKey) throws JiraException { + Set instances = jiraService.getAvailableInstances(); + + if (instances == null || instances.isEmpty()) { + return jiraService.projectExists(projectKey); + } + + for (String instanceName : instances.stream().sorted(Comparator.naturalOrder()).toList()) { + if (jiraService.projectExists(instanceName, projectKey)) { + return true; + } + } + + return false; + } + + private boolean existsInAnyOpenshift(String projectKey) throws OpenshiftException { + Set instances = openshiftService.getAvailableInstances(); + + if (instances == null || instances.isEmpty()) { + return false; + } + + for (String instanceName : instances.stream().sorted(Comparator.naturalOrder()).toList()) { + if (openshiftService.projectExists(instanceName, projectKey)) { + return true; + } + } + + return false; + } +} diff --git a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java new file mode 100644 index 0000000..09b8f10 --- /dev/null +++ b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java @@ -0,0 +1,48 @@ +package org.opendevstack.apiservice.serviceproject.service.impl; + +import lombok.extern.slf4j.Slf4j; +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.serviceproject.service.GenerateProjectKeyService; +import org.opendevstack.apiservice.serviceproject.service.ProjectService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +public class ProjectServiceImpl implements ProjectService { + + private final OpenshiftService openshiftService; + + private final BitbucketService bitbucketService; + + private final JiraService jiraService; + + private final GenerateProjectKeyService generateProjectKeyService; + + @Autowired + public ProjectServiceImpl(BitbucketService bitbucketService, JiraService jiraService, + OpenshiftService openshiftService, + GenerateProjectKeyService generateProjectKeyService) { + this.bitbucketService = bitbucketService; + this.jiraService = jiraService; + this.openshiftService = openshiftService; + this.generateProjectKeyService = generateProjectKeyService; + } + + @Override + public CreateProjectResponse createProject(CreateProjectRequest request) { + // TODO Implement project creation against external systems. + return null; + } + + @Override + public CreateProjectResponse getProject(String projectKey) { + // TODO Implement project retrieval by key from external systems. + return null; + } +} + diff --git a/external-service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImplTest.java b/external-service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImplTest.java new file mode 100644 index 0000000..b9cf5a0 --- /dev/null +++ b/external-service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImplTest.java @@ -0,0 +1,92 @@ +package org.opendevstack.apiservice.serviceproject.service.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +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.exception.ProjectKeyGenerationException; + +import java.util.Random; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class GenerateProjectKeyServiceImplTest { + + @Mock + private BitbucketService bitbucketService; + + @Mock + private JiraService jiraService; + + @Mock + private OpenshiftService openshiftService; + + @Mock + private Random random; + + private GenerateProjectKeyServiceImpl tested; + + @BeforeEach + void setup() { + tested = new GenerateProjectKeyServiceImpl(bitbucketService, jiraService, openshiftService, random); + when(bitbucketService.getAvailableInstances()).thenReturn(Set.of("dev")); + when(jiraService.getAvailableInstances()).thenReturn(Set.of("default")); + when(openshiftService.getAvailableInstances()).thenReturn(Set.of()); + } + + @Test + void generateProjectKey_whenFirstCandidateIsFree_thenReturnKey() throws Exception { + when(random.nextInt(1_000_000)).thenReturn(7); + when(bitbucketService.projectExists(anyString(), anyString())).thenReturn(false); + when(jiraService.projectExists(anyString(), anyString())).thenReturn(false); + + String result = tested.generateProjectKey(null); + + assertThat(result).isEqualTo("SS000007"); + } + + @Test + void generateProjectKey_whenFirstCandidateExists_thenRetryUntilUnique() throws Exception { + when(random.nextInt(1_000_000)).thenReturn(1, 2); + when(bitbucketService.projectExists(anyString(), anyString())).thenReturn(true, false); + when(jiraService.projectExists(anyString(), anyString())).thenReturn(true, false); + + String result = tested.generateProjectKey("SS%06d"); + + assertThat(result).isEqualTo("SS000002"); + } + + @Test + void generateProjectKey_whenNoUniqueKeyAfterMaxRetries_thenThrowException() throws Exception { + when(random.nextInt(1_000_000)).thenReturn(1); + when(bitbucketService.projectExists(anyString(), anyString())).thenReturn(true); + when(jiraService.projectExists(anyString(), anyString())).thenReturn(true); + + assertThatThrownBy(() -> tested.generateProjectKey("SS%06d")) + .isInstanceOf(ProjectKeyGenerationException.class) + .hasMessageContaining("Failed to generate unique project key after 10 retries"); + } + + @Test + void generateProjectKey_whenCustomPatternProvided_thenUseIt() throws Exception { + when(random.nextInt(1_000_000)).thenReturn(42); + when(bitbucketService.projectExists(anyString(), anyString())).thenReturn(false); + when(jiraService.projectExists(anyString(), anyString())).thenReturn(false); + + String result = tested.generateProjectKey("AB%04d"); + + assertThat(result).isEqualTo("AB0042"); + } +} diff --git a/external-service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java b/external-service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java new file mode 100644 index 0000000..fcb48f1 --- /dev/null +++ b/external-service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java @@ -0,0 +1,34 @@ +package org.opendevstack.apiservice.serviceproject.service.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +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.service.GenerateProjectKeyService; + +@ExtendWith(MockitoExtension.class) +class ProjectServiceImplTest { + + @Mock + private BitbucketService bitbucketService; + + @Mock + private JiraService jiraService; + + @Mock + private OpenshiftService openshiftService; + + @Mock + private GenerateProjectKeyService generateProjectKeyService; + + private ProjectServiceImpl sut; + + @BeforeEach + void setup() { + sut = new ProjectServiceImpl(bitbucketService, jiraService, openshiftService, generateProjectKeyService); + } +} + From 77a4dd8a6f7c88325a7a1ac31ce61e9245a936cc Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Thu, 5 Mar 2026 13:11:16 +0100 Subject: [PATCH 02/14] Bump version to 0.0.3 in pom.xml for all modules --- external-service-projects/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/external-service-projects/pom.xml b/external-service-projects/pom.xml index c6d622e..ca9fd77 100644 --- a/external-service-projects/pom.xml +++ b/external-service-projects/pom.xml @@ -6,7 +6,7 @@ org.opendevstack.apiservice devstack-api-service - 0.0.2 + 0.0.3 external-service-projects From e6fd774bb8c60f73ea0db475657e469032fcab9b Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Thu, 5 Mar 2026 15:59:49 +0100 Subject: [PATCH 03/14] Rename external service project files and update artifactId in pom.xml --- external-service-projects/pom.xml | 69 --------- .../ProjectKeyGenerationException.java | 13 -- .../model/CreateProjectRequest.java | 17 --- .../model/CreateProjectResponse.java | 21 --- .../service/GenerateProjectKeyService.java | 11 -- .../service/ProjectService.java | 12 -- .../impl/GenerateProjectKeyServiceImpl.java | 144 ------------------ .../service/impl/ProjectServiceImpl.java | 48 ------ .../GenerateProjectKeyServiceImplTest.java | 92 ----------- .../service/impl/ProjectServiceImplTest.java | 68 ++++----- 10 files changed, 34 insertions(+), 461 deletions(-) delete mode 100644 external-service-projects/pom.xml delete mode 100644 external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/exception/ProjectKeyGenerationException.java delete mode 100644 external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectRequest.java delete mode 100644 external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectResponse.java delete mode 100644 external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/GenerateProjectKeyService.java delete mode 100644 external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java delete mode 100644 external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImpl.java delete mode 100644 external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java delete mode 100644 external-service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImplTest.java rename {external-service-projects => service-projects}/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java (96%) diff --git a/external-service-projects/pom.xml b/external-service-projects/pom.xml deleted file mode 100644 index ca9fd77..0000000 --- a/external-service-projects/pom.xml +++ /dev/null @@ -1,69 +0,0 @@ - - 4.0.0 - - - org.opendevstack.apiservice - devstack-api-service - 0.0.3 - - - external-service-projects - External Service Projects - Service module for project operations: key generation and project existence checks - - - - org.springframework.boot - spring-boot-starter-web - - - - org.springframework.boot - spring-boot-starter-validation - - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - - - - org.openapitools - jackson-databind-nullable - ${jackson-databind-nullable.version} - - - - org.opendevstack.apiservice - external-service-bitbucket - ${project.version} - - - - org.opendevstack.apiservice - external-service-jira - ${project.version} - - - - org.opendevstack.apiservice - external-service-ocp - ${project.version} - - - - org.projectlombok - lombok - provided - - - - org.springframework.boot - spring-boot-starter-test - test - - - - diff --git a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/exception/ProjectKeyGenerationException.java b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/exception/ProjectKeyGenerationException.java deleted file mode 100644 index dad4ffa..0000000 --- a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/exception/ProjectKeyGenerationException.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.opendevstack.apiservice.serviceproject.exception; - -public class ProjectKeyGenerationException extends Exception { - - public ProjectKeyGenerationException(String message) { - super(message); - } - - public ProjectKeyGenerationException(String message, Throwable cause) { - super(message, cause); - } -} - diff --git a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectRequest.java b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectRequest.java deleted file mode 100644 index 6127031..0000000 --- a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.opendevstack.apiservice.serviceproject.model; - -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class CreateProjectRequest { - - private String projectKey; - - private String projectKeyPattern; - - private String projectName; - - private String projectDescription; -} diff --git a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectResponse.java b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectResponse.java deleted file mode 100644 index 0775c9b..0000000 --- a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectResponse.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.opendevstack.apiservice.serviceproject.model; - -import lombok.AllArgsConstructor; -import lombok.Data; - -@Data -@AllArgsConstructor -public class CreateProjectResponse { - - private String projectKey; - - private String status; - - private String message; - - private String error; - - private String errorKey; - - private String errorDescription; -} diff --git a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/GenerateProjectKeyService.java b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/GenerateProjectKeyService.java deleted file mode 100644 index 33ffda0..0000000 --- a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/GenerateProjectKeyService.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.opendevstack.apiservice.serviceproject.service; - -import org.opendevstack.apiservice.serviceproject.exception.ProjectKeyGenerationException; - -public interface GenerateProjectKeyService { - - String DEFAULT_PROJECT_KEY_PATTERN = "SS%06d"; - - String generateProjectKey(String projectKeyPattern) throws ProjectKeyGenerationException; -} - diff --git a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java deleted file mode 100644 index 0c59b5e..0000000 --- a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.opendevstack.apiservice.serviceproject.service; - -import org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest; -import org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse; - -public interface ProjectService { - - CreateProjectResponse createProject(CreateProjectRequest request); - - CreateProjectResponse getProject(String projectKey); -} - diff --git a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImpl.java b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImpl.java deleted file mode 100644 index c70b85d..0000000 --- a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImpl.java +++ /dev/null @@ -1,144 +0,0 @@ -package org.opendevstack.apiservice.serviceproject.service.impl; - -import lombok.extern.slf4j.Slf4j; -import org.opendevstack.apiservice.externalservice.bitbucket.exception.BitbucketException; -import org.opendevstack.apiservice.externalservice.bitbucket.service.BitbucketService; -import org.opendevstack.apiservice.externalservice.jira.exception.JiraException; -import org.opendevstack.apiservice.externalservice.jira.service.JiraService; -import org.opendevstack.apiservice.externalservice.ocp.exception.OpenshiftException; -import org.opendevstack.apiservice.externalservice.ocp.service.OpenshiftService; -import org.opendevstack.apiservice.serviceproject.exception.ProjectKeyGenerationException; -import org.opendevstack.apiservice.serviceproject.service.GenerateProjectKeyService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.util.Comparator; -import java.util.Random; -import java.util.Set; - -@Service -@Slf4j -public class GenerateProjectKeyServiceImpl implements GenerateProjectKeyService { - - private static final int MAX_RETRIES = 10; - - private final OpenshiftService openshiftService; - - private final BitbucketService bitbucketService; - - private final JiraService jiraService; - - private final Random random; - - @Autowired - public GenerateProjectKeyServiceImpl(BitbucketService bitbucketService, JiraService jiraService, - OpenshiftService openshiftService) { - this(bitbucketService, jiraService, openshiftService, new Random()); - } - - GenerateProjectKeyServiceImpl(BitbucketService bitbucketService, JiraService jiraService, - OpenshiftService openshiftService, Random random) { - this.bitbucketService = bitbucketService; - this.jiraService = jiraService; - this.openshiftService = openshiftService; - this.random = random; - } - - @Override - public String generateProjectKey(String projectKeyPattern) throws ProjectKeyGenerationException { - String pattern = resolveProjectKeyPattern(projectKeyPattern); - - for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) { - int randomNumber = random.nextInt(1_000_000); - String projectKey = String.format(pattern, randomNumber); - - if (!isProjectFound(projectKey)) { - log.debug("Generated unique project key '{}' on attempt {}", projectKey, attempt); - return projectKey; - } - - log.debug("Project key '{}' already exists (attempt {}/{})", projectKey, attempt, MAX_RETRIES); - } - - throw new ProjectKeyGenerationException( - String.format("Failed to generate unique project key after %d retries", MAX_RETRIES)); - } - - private String resolveProjectKeyPattern(String projectKeyPattern) { - if (projectKeyPattern == null || projectKeyPattern.isBlank()) { - return DEFAULT_PROJECT_KEY_PATTERN; - } - return projectKeyPattern; - } - - private boolean isProjectFound(String projectKey) throws ProjectKeyGenerationException { - try { - if (existsInAnyBitbucketInstance(projectKey)) { - return true; - } - - if (existsInAnyJiraInstance(projectKey)) { - return true; - } - - if (existsInAnyOpenshift(projectKey)) { - return true; - } - - return false; - } catch (BitbucketException e) { - throw new ProjectKeyGenerationException( - String.format("Failed to check project '%s' in Bitbucket", projectKey), e); - } catch (JiraException e) { - throw new ProjectKeyGenerationException( - String.format("Failed to check project '%s' in Jira", projectKey), e); - } catch (OpenshiftException e) { - throw new ProjectKeyGenerationException( - String.format("Failed to check project '%s' in Openshift", projectKey), e); - } - } - - private boolean existsInAnyBitbucketInstance(String projectKey) throws BitbucketException { - Set instances = bitbucketService.getAvailableInstances(); - - for (String instanceName : instances.stream().sorted(Comparator.naturalOrder()).toList()) { - if (bitbucketService.projectExists(instanceName, projectKey)) { - return true; - } - } - - return false; - } - - private boolean existsInAnyJiraInstance(String projectKey) throws JiraException { - Set instances = jiraService.getAvailableInstances(); - - if (instances == null || instances.isEmpty()) { - return jiraService.projectExists(projectKey); - } - - for (String instanceName : instances.stream().sorted(Comparator.naturalOrder()).toList()) { - if (jiraService.projectExists(instanceName, projectKey)) { - return true; - } - } - - return false; - } - - private boolean existsInAnyOpenshift(String projectKey) throws OpenshiftException { - Set instances = openshiftService.getAvailableInstances(); - - if (instances == null || instances.isEmpty()) { - return false; - } - - for (String instanceName : instances.stream().sorted(Comparator.naturalOrder()).toList()) { - if (openshiftService.projectExists(instanceName, projectKey)) { - return true; - } - } - - return false; - } -} diff --git a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java b/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java deleted file mode 100644 index 09b8f10..0000000 --- a/external-service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.opendevstack.apiservice.serviceproject.service.impl; - -import lombok.extern.slf4j.Slf4j; -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.serviceproject.service.GenerateProjectKeyService; -import org.opendevstack.apiservice.serviceproject.service.ProjectService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -@Service -@Slf4j -public class ProjectServiceImpl implements ProjectService { - - private final OpenshiftService openshiftService; - - private final BitbucketService bitbucketService; - - private final JiraService jiraService; - - private final GenerateProjectKeyService generateProjectKeyService; - - @Autowired - public ProjectServiceImpl(BitbucketService bitbucketService, JiraService jiraService, - OpenshiftService openshiftService, - GenerateProjectKeyService generateProjectKeyService) { - this.bitbucketService = bitbucketService; - this.jiraService = jiraService; - this.openshiftService = openshiftService; - this.generateProjectKeyService = generateProjectKeyService; - } - - @Override - public CreateProjectResponse createProject(CreateProjectRequest request) { - // TODO Implement project creation against external systems. - return null; - } - - @Override - public CreateProjectResponse getProject(String projectKey) { - // TODO Implement project retrieval by key from external systems. - return null; - } -} - diff --git a/external-service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImplTest.java b/external-service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImplTest.java deleted file mode 100644 index b9cf5a0..0000000 --- a/external-service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImplTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package org.opendevstack.apiservice.serviceproject.service.impl; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; -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.exception.ProjectKeyGenerationException; - -import java.util.Random; -import java.util.Set; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -class GenerateProjectKeyServiceImplTest { - - @Mock - private BitbucketService bitbucketService; - - @Mock - private JiraService jiraService; - - @Mock - private OpenshiftService openshiftService; - - @Mock - private Random random; - - private GenerateProjectKeyServiceImpl tested; - - @BeforeEach - void setup() { - tested = new GenerateProjectKeyServiceImpl(bitbucketService, jiraService, openshiftService, random); - when(bitbucketService.getAvailableInstances()).thenReturn(Set.of("dev")); - when(jiraService.getAvailableInstances()).thenReturn(Set.of("default")); - when(openshiftService.getAvailableInstances()).thenReturn(Set.of()); - } - - @Test - void generateProjectKey_whenFirstCandidateIsFree_thenReturnKey() throws Exception { - when(random.nextInt(1_000_000)).thenReturn(7); - when(bitbucketService.projectExists(anyString(), anyString())).thenReturn(false); - when(jiraService.projectExists(anyString(), anyString())).thenReturn(false); - - String result = tested.generateProjectKey(null); - - assertThat(result).isEqualTo("SS000007"); - } - - @Test - void generateProjectKey_whenFirstCandidateExists_thenRetryUntilUnique() throws Exception { - when(random.nextInt(1_000_000)).thenReturn(1, 2); - when(bitbucketService.projectExists(anyString(), anyString())).thenReturn(true, false); - when(jiraService.projectExists(anyString(), anyString())).thenReturn(true, false); - - String result = tested.generateProjectKey("SS%06d"); - - assertThat(result).isEqualTo("SS000002"); - } - - @Test - void generateProjectKey_whenNoUniqueKeyAfterMaxRetries_thenThrowException() throws Exception { - when(random.nextInt(1_000_000)).thenReturn(1); - when(bitbucketService.projectExists(anyString(), anyString())).thenReturn(true); - when(jiraService.projectExists(anyString(), anyString())).thenReturn(true); - - assertThatThrownBy(() -> tested.generateProjectKey("SS%06d")) - .isInstanceOf(ProjectKeyGenerationException.class) - .hasMessageContaining("Failed to generate unique project key after 10 retries"); - } - - @Test - void generateProjectKey_whenCustomPatternProvided_thenUseIt() throws Exception { - when(random.nextInt(1_000_000)).thenReturn(42); - when(bitbucketService.projectExists(anyString(), anyString())).thenReturn(false); - when(jiraService.projectExists(anyString(), anyString())).thenReturn(false); - - String result = tested.generateProjectKey("AB%04d"); - - assertThat(result).isEqualTo("AB0042"); - } -} diff --git a/external-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 similarity index 96% rename from external-service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java rename to service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java index fcb48f1..0779419 100644 --- a/external-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 @@ -1,34 +1,34 @@ -package org.opendevstack.apiservice.serviceproject.service.impl; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -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.service.GenerateProjectKeyService; - -@ExtendWith(MockitoExtension.class) -class ProjectServiceImplTest { - - @Mock - private BitbucketService bitbucketService; - - @Mock - private JiraService jiraService; - - @Mock - private OpenshiftService openshiftService; - - @Mock - private GenerateProjectKeyService generateProjectKeyService; - - private ProjectServiceImpl sut; - - @BeforeEach - void setup() { - sut = new ProjectServiceImpl(bitbucketService, jiraService, openshiftService, generateProjectKeyService); - } -} - +package org.opendevstack.apiservice.serviceproject.service.impl; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +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.service.GenerateProjectKeyService; + +@ExtendWith(MockitoExtension.class) +class ProjectServiceImplTest { + + @Mock + private BitbucketService bitbucketService; + + @Mock + private JiraService jiraService; + + @Mock + private OpenshiftService openshiftService; + + @Mock + private GenerateProjectKeyService generateProjectKeyService; + + private ProjectServiceImpl sut; + + @BeforeEach + void setup() { + sut = new ProjectServiceImpl(bitbucketService, jiraService, openshiftService, generateProjectKeyService); + } +} + From 059a5d309115cab8cc919672239f390f4b3fefb0 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Fri, 6 Mar 2026 17:38:19 +0100 Subject: [PATCH 04/14] Add API module for project management with create and retrieve functionality --- .../ProjectControllerIntegrationTest.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerIntegrationTest.java 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..93b82c1 --- /dev/null +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerIntegrationTest.java @@ -0,0 +1,81 @@ +package org.opendevstack.apiservice.project.controller; + +import org.junit.jupiter.api.Test; +import org.mapstruct.factory.Mappers; +import org.opendevstack.apiservice.project.facade.impl.ProjectsFacadeImpl; +import org.opendevstack.apiservice.project.mapper.ProjectMapper; +import org.opendevstack.apiservice.serviceproject.service.ProjectService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +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.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; + + @Autowired + @MockitoBean + private ProjectService projectService; + + @Test + void createProject_withProvidedProjectKey_returnsInitiatedResponse() throws Exception { + org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse serviceResponse = + new org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse(); + serviceResponse.setProjectKey("PROJ01"); + serviceResponse.setStatus("Initiated"); + when(projectService.createProject(org.mockito.ArgumentMatchers.any( + org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest.class))) + .thenReturn(serviceResponse); + + String payload = """ + { + \"projectKey\": \"PROJ01\", + \"projectName\": \"My Project\", + \"projectDescription\": \"desc\" + } + """; + + mockMvc.perform(post("/api/v0/projects") + .contentType("application/json") + .content(payload == null ? "" : payload)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.projectKey").value("PROJ01")) + .andExpect(jsonPath("$.status").value("Initiated")); + } + + @Test + void getProject_whenNotFound_returns404() throws Exception { + when(projectService.getProject("UNKNOWN")).thenReturn(null); + + mockMvc.perform(get("/api/v0/projects/UNKNOWN")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.error").value("NOT_FOUND")) + .andExpect(jsonPath("$.errorKey").value("PROJECT_NOT_FOUND")); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + @Import({ProjectController.class, ProjectsFacadeImpl.class}) + static class TestConfig { + + @Bean + ProjectMapper projectMapper() { + return Mappers.getMapper(ProjectMapper.class); + } + } +} \ No newline at end of file From bd6f019b15907a06697472a13590b307a819c065 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Thu, 12 Mar 2026 11:19:23 +0100 Subject: [PATCH 05/14] Update API base path to /api/pub/v0 and refactor ProjectServiceImpl constructor --- .../ProjectControllerIntegrationTest.java | 81 ------------------- .../service/impl/ProjectServiceImplTest.java | 34 -------- 2 files changed, 115 deletions(-) delete mode 100644 api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerIntegrationTest.java delete mode 100644 service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java 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 deleted file mode 100644 index 93b82c1..0000000 --- a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerIntegrationTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package org.opendevstack.apiservice.project.controller; - -import org.junit.jupiter.api.Test; -import org.mapstruct.factory.Mappers; -import org.opendevstack.apiservice.project.facade.impl.ProjectsFacadeImpl; -import org.opendevstack.apiservice.project.mapper.ProjectMapper; -import org.opendevstack.apiservice.serviceproject.service.ProjectService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.SpringBootConfiguration; -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Bean; -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.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; - - @Autowired - @MockitoBean - private ProjectService projectService; - - @Test - void createProject_withProvidedProjectKey_returnsInitiatedResponse() throws Exception { - org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse serviceResponse = - new org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse(); - serviceResponse.setProjectKey("PROJ01"); - serviceResponse.setStatus("Initiated"); - when(projectService.createProject(org.mockito.ArgumentMatchers.any( - org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest.class))) - .thenReturn(serviceResponse); - - String payload = """ - { - \"projectKey\": \"PROJ01\", - \"projectName\": \"My Project\", - \"projectDescription\": \"desc\" - } - """; - - mockMvc.perform(post("/api/v0/projects") - .contentType("application/json") - .content(payload == null ? "" : payload)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.projectKey").value("PROJ01")) - .andExpect(jsonPath("$.status").value("Initiated")); - } - - @Test - void getProject_whenNotFound_returns404() throws Exception { - when(projectService.getProject("UNKNOWN")).thenReturn(null); - - mockMvc.perform(get("/api/v0/projects/UNKNOWN")) - .andExpect(status().isNotFound()) - .andExpect(jsonPath("$.error").value("NOT_FOUND")) - .andExpect(jsonPath("$.errorKey").value("PROJECT_NOT_FOUND")); - } - - @SpringBootConfiguration - @EnableAutoConfiguration - @Import({ProjectController.class, ProjectsFacadeImpl.class}) - static class TestConfig { - - @Bean - ProjectMapper projectMapper() { - return Mappers.getMapper(ProjectMapper.class); - } - } -} \ No newline at end of file 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 deleted file mode 100644 index 0779419..0000000 --- a/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.opendevstack.apiservice.serviceproject.service.impl; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -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.service.GenerateProjectKeyService; - -@ExtendWith(MockitoExtension.class) -class ProjectServiceImplTest { - - @Mock - private BitbucketService bitbucketService; - - @Mock - private JiraService jiraService; - - @Mock - private OpenshiftService openshiftService; - - @Mock - private GenerateProjectKeyService generateProjectKeyService; - - private ProjectServiceImpl sut; - - @BeforeEach - void setup() { - sut = new ProjectServiceImpl(bitbucketService, jiraService, openshiftService, generateProjectKeyService); - } -} - From ef84f80cf2ea693e9f7048c46cf76edf002fe8eb Mon Sep 17 00:00:00 2001 From: Jorge Romero Date: Tue, 3 Mar 2026 15:15:31 +0100 Subject: [PATCH 06/14] Add database module with Liquibase integration and JPA Repository --- .../src/test/resources/application.properties | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 persistence/src/test/resources/application.properties 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 From e53a8108c901c58ba07c2574a660c7705571fc86 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Fri, 6 Mar 2026 17:38:19 +0100 Subject: [PATCH 07/14] Add API module for project management with create and retrieve functionality --- .../ProjectControllerIntegrationTest.java | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerIntegrationTest.java 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..93b82c1 --- /dev/null +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerIntegrationTest.java @@ -0,0 +1,81 @@ +package org.opendevstack.apiservice.project.controller; + +import org.junit.jupiter.api.Test; +import org.mapstruct.factory.Mappers; +import org.opendevstack.apiservice.project.facade.impl.ProjectsFacadeImpl; +import org.opendevstack.apiservice.project.mapper.ProjectMapper; +import org.opendevstack.apiservice.serviceproject.service.ProjectService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Bean; +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.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; + + @Autowired + @MockitoBean + private ProjectService projectService; + + @Test + void createProject_withProvidedProjectKey_returnsInitiatedResponse() throws Exception { + org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse serviceResponse = + new org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse(); + serviceResponse.setProjectKey("PROJ01"); + serviceResponse.setStatus("Initiated"); + when(projectService.createProject(org.mockito.ArgumentMatchers.any( + org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest.class))) + .thenReturn(serviceResponse); + + String payload = """ + { + \"projectKey\": \"PROJ01\", + \"projectName\": \"My Project\", + \"projectDescription\": \"desc\" + } + """; + + mockMvc.perform(post("/api/v0/projects") + .contentType("application/json") + .content(payload == null ? "" : payload)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.projectKey").value("PROJ01")) + .andExpect(jsonPath("$.status").value("Initiated")); + } + + @Test + void getProject_whenNotFound_returns404() throws Exception { + when(projectService.getProject("UNKNOWN")).thenReturn(null); + + mockMvc.perform(get("/api/v0/projects/UNKNOWN")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.error").value("NOT_FOUND")) + .andExpect(jsonPath("$.errorKey").value("PROJECT_NOT_FOUND")); + } + + @SpringBootConfiguration + @EnableAutoConfiguration + @Import({ProjectController.class, ProjectsFacadeImpl.class}) + static class TestConfig { + + @Bean + ProjectMapper projectMapper() { + return Mappers.getMapper(ProjectMapper.class); + } + } +} \ No newline at end of file From 8760581f74e61230b169fd5dc7748d80dc0348d8 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Thu, 12 Mar 2026 15:59:50 +0100 Subject: [PATCH 08/14] Refactor project service integration tests and enhance response handling --- .../ProjectControllerIntegrationTest.java | 48 +++++++++++-------- .../controller/ProjectControllerTest.java | 17 +++++++ .../core/DevstackApiServiceApplication.java | 4 ++ .../mapper/CreateProjectResponseMapper.java | 16 +++++++ 4 files changed, 64 insertions(+), 21 deletions(-) create mode 100644 service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/CreateProjectResponseMapper.java 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 index 93b82c1..ad14d30 100644 --- 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 @@ -1,20 +1,22 @@ package org.opendevstack.apiservice.project.controller; import org.junit.jupiter.api.Test; -import org.mapstruct.factory.Mappers; -import org.opendevstack.apiservice.project.facade.impl.ProjectsFacadeImpl; -import org.opendevstack.apiservice.project.mapper.ProjectMapper; -import org.opendevstack.apiservice.serviceproject.service.ProjectService; +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.Bean; 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; @@ -28,18 +30,16 @@ class ProjectControllerIntegrationTest { @Autowired private MockMvc mockMvc; - @Autowired @MockitoBean - private ProjectService projectService; + private ProjectsFacade facade; @Test void createProject_withProvidedProjectKey_returnsInitiatedResponse() throws Exception { - org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse serviceResponse = - new org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse(); + CreateProjectResponse serviceResponse = + new CreateProjectResponse(); serviceResponse.setProjectKey("PROJ01"); serviceResponse.setStatus("Initiated"); - when(projectService.createProject(org.mockito.ArgumentMatchers.any( - org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest.class))) + when(facade.createProject(any(CreateProjectRequest.class))) .thenReturn(serviceResponse); String payload = """ @@ -55,27 +55,33 @@ void createProject_withProvidedProjectKey_returnsInitiatedResponse() throws Exce .content(payload == null ? "" : payload)) .andExpect(status().isOk()) .andExpect(jsonPath("$.projectKey").value("PROJ01")) - .andExpect(jsonPath("$.status").value("Initiated")); + .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(projectService.getProject("UNKNOWN")).thenReturn(null); + when(facade.getProject("UNKNOWN")).thenReturn(null); mockMvc.perform(get("/api/v0/projects/UNKNOWN")) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.error").value("NOT_FOUND")) - .andExpect(jsonPath("$.errorKey").value("PROJECT_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 - @Import({ProjectController.class, ProjectsFacadeImpl.class}) + @EnableAutoConfiguration(exclude = { + DataSourceAutoConfiguration.class, + DataSourceTransactionManagerAutoConfiguration.class, + HibernateJpaAutoConfiguration.class + }) + @Import({ProjectController.class}) static class TestConfig { - - @Bean - ProjectMapper projectMapper() { - return Mappers.getMapper(ProjectMapper.class); - } } } \ 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/src/main/java/org/opendevstack/apiservice/core/DevstackApiServiceApplication.java b/core/src/main/java/org/opendevstack/apiservice/core/DevstackApiServiceApplication.java index bcb4866..564996e 100644 --- a/core/src/main/java/org/opendevstack/apiservice/core/DevstackApiServiceApplication.java +++ b/core/src/main/java/org/opendevstack/apiservice/core/DevstackApiServiceApplication.java @@ -2,9 +2,13 @@ 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" }) +@EnableJpaRepositories(basePackages = "org.opendevstack.apiservice.persistence.repository") +@EntityScan(basePackages = "org.opendevstack.apiservice.persistence.entity") @EnableCaching public class DevstackApiServiceApplication { 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); +} From 54dd88f1b1a520651a8f63cfefeb650a4b905d69 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Thu, 12 Mar 2026 16:46:09 +0100 Subject: [PATCH 09/14] Update API base path in integration tests to /api/pub/v0 and add new dependencies in pom.xml --- .../controller/ProjectControllerIntegrationTest.java | 4 ++-- service-projects/pom.xml | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) 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 index ad14d30..2ee32d5 100644 --- 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 @@ -50,7 +50,7 @@ void createProject_withProvidedProjectKey_returnsInitiatedResponse() throws Exce } """; - mockMvc.perform(post("/api/v0/projects") + mockMvc.perform(post("/api/pub/v0/projects") .contentType("application/json") .content(payload == null ? "" : payload)) .andExpect(status().isOk()) @@ -65,7 +65,7 @@ void createProject_withProvidedProjectKey_returnsInitiatedResponse() throws Exce void getProject_whenNotFound_returns404() throws Exception { when(facade.getProject("UNKNOWN")).thenReturn(null); - mockMvc.perform(get("/api/v0/projects/UNKNOWN")) + mockMvc.perform(get("/api/pub/v0/projects/UNKNOWN")) .andExpect(status().isNotFound()) .andExpect(jsonPath("$.error").value("NOT_FOUND")) .andExpect(jsonPath("$.errorKey").value("PROJECT_NOT_FOUND")) diff --git a/service-projects/pom.xml b/service-projects/pom.xml index cd1a931..61a9d29 100644 --- a/service-projects/pom.xml +++ b/service-projects/pom.xml @@ -35,6 +35,18 @@ ${jackson-databind-nullable.version} + + org.mapstruct + mapstruct + 1.6.3 + + + + org.opendevstack.apiservice + persistence + ${project.version} + + org.opendevstack.apiservice external-service-api From 6fd036710db5b4fc32d8bf8a09e31b3c5caa5e1c Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Thu, 12 Mar 2026 16:50:15 +0100 Subject: [PATCH 10/14] Add Spring Boot DevTools dependency for improved development experience --- core/pom.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/pom.xml b/core/pom.xml index 6705b44..89b9175 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 From fbd572d39d4433af57141d62e1b9e41bcd313783 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Thu, 12 Mar 2026 17:46:26 +0100 Subject: [PATCH 11/14] Add project service implementation with create and retrieve functionality, and include unit tests --- core/pom.xml | 7 + service-projects/pom.xml | 23 ++ .../service/impl/ProjectServiceImpl.java | 17 +- .../service/impl/ProjectServiceImplTest.java | 204 ++++++++++++++++++ 4 files changed, 250 insertions(+), 1 deletion(-) create mode 100644 service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java diff --git a/core/pom.xml b/core/pom.xml index 89b9175..c3aa14b 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -127,6 +127,13 @@ ${project.version} + + + org.opendevstack.apiservice + service-projects + ${project.version} + + org.opendevstack.apiservice diff --git a/service-projects/pom.xml b/service-projects/pom.xml index 61a9d29..ae4f309 100644 --- a/service-projects/pom.xml +++ b/service-projects/pom.xml @@ -83,5 +83,28 @@ test + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + ${lombok.version} + + + org.mapstruct + mapstruct-processor + 1.6.3 + + + + + + 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..141d519 --- /dev/null +++ b/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java @@ -0,0 +1,204 @@ +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.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +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); + } + + @Test + void get_project_does_not_call_external_services() { + // GIVEN + String projectKey = "API-TEST"; + + when(projectRepository.findByProjectKey(projectKey)).thenReturn(Optional.empty()); + + // WHEN + projectService.getProject(projectKey); + + // THEN + verify(openshiftService, never()).getProject(any()); + verify(bitbucketService, never()).getProject(any()); + verify(jiraService, never()).getProject(any()); + } + +} \ No newline at end of file From 2099073f9b8cfa5f222a9a412d101d453760f11b Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Thu, 12 Mar 2026 17:53:23 +0100 Subject: [PATCH 12/14] Refactor MapStruct versioning in pom.xml and update ProjectServiceImplTest assertions --- api-project/pom.xml | 4 ++-- .../apiservice/core/DevstackApiServiceApplication.java | 4 ---- pom.xml | 1 + service-projects/pom.xml | 4 ++-- .../service/impl/ProjectServiceImplTest.java | 9 +++++++-- 5 files changed, 12 insertions(+), 10 deletions(-) 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/core/src/main/java/org/opendevstack/apiservice/core/DevstackApiServiceApplication.java b/core/src/main/java/org/opendevstack/apiservice/core/DevstackApiServiceApplication.java index 564996e..bcb4866 100644 --- a/core/src/main/java/org/opendevstack/apiservice/core/DevstackApiServiceApplication.java +++ b/core/src/main/java/org/opendevstack/apiservice/core/DevstackApiServiceApplication.java @@ -2,13 +2,9 @@ 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" }) -@EnableJpaRepositories(basePackages = "org.opendevstack.apiservice.persistence.repository") -@EntityScan(basePackages = "org.opendevstack.apiservice.persistence.entity") @EnableCaching public class DevstackApiServiceApplication { 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 ae4f309..2a82727 100644 --- a/service-projects/pom.xml +++ b/service-projects/pom.xml @@ -38,7 +38,7 @@ org.mapstruct mapstruct - 1.6.3 + ${mapstruct.version} @@ -99,7 +99,7 @@ org.mapstruct mapstruct-processor - 1.6.3 + ${mapstruct.version} 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 141d519..7039a16 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 @@ -17,9 +17,14 @@ import java.util.Optional; import java.util.UUID; -import static org.junit.jupiter.api.Assertions.*; +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.*; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; class ProjectServiceImplTest { From 82279e149d80f272987f090a720454bfc7142418 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Fri, 13 Mar 2026 08:50:19 +0100 Subject: [PATCH 13/14] Remove unused test for external service calls in ProjectServiceImplTest --- .../service/impl/ProjectServiceImplTest.java | 17 ----------------- 1 file changed, 17 deletions(-) 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 7039a16..e9db6ae 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 @@ -189,21 +189,4 @@ void get_project_returns_response_for_soft_deleted_project() { assertEquals(projectKey, result.getProjectKey()); verify(projectRepository).findByProjectKey(projectKey); } - - @Test - void get_project_does_not_call_external_services() { - // GIVEN - String projectKey = "API-TEST"; - - when(projectRepository.findByProjectKey(projectKey)).thenReturn(Optional.empty()); - - // WHEN - projectService.getProject(projectKey); - - // THEN - verify(openshiftService, never()).getProject(any()); - verify(bitbucketService, never()).getProject(any()); - verify(jiraService, never()).getProject(any()); - } - } \ No newline at end of file From 8ffa571bbc0656616905a2862d8e8f44ffe152bb Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Fri, 13 Mar 2026 15:06:50 +0100 Subject: [PATCH 14/14] Add JPA configuration to DevstackApiServiceApplication and remove from PersistenceTestApplication --- .../apiservice/core/DevstackApiServiceApplication.java | 4 ++++ .../apiservice/persistence/PersistenceTestApplication.java | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) 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