From 2a626d1e260fd0911236dffdf901dff2a54f5ed8 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Thu, 5 Mar 2026 12:04:01 +0100 Subject: [PATCH 01/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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/20] 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 From 1d81b0f7dd786333ffad6ba15a995550a8f4dbf2 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Mon, 16 Mar 2026 16:49:53 +0100 Subject: [PATCH 15/20] Refactor project request and response models, update mappings, and add global Jackson configuration --- api-project/openapi/api-project.yaml | 2 ++ .../project/mapper/ProjectMapper.java | 27 ++++++++++++--- .../facade/impl/ProjectsFacadeImplTest.java | 16 +++++---- .../apiservice/core/config/JacksonConfig.java | 21 ++++++++++++ .../mapper/CreateProjectResponseMapper.java | 16 --------- .../mapper/ProjectResponseMapper.java | 11 ++++++ ...rojectRequest.java => ProjectRequest.java} | 2 +- ...jectResponse.java => ProjectResponse.java} | 10 +----- .../service/ProjectService.java | 8 ++--- .../service/impl/ProjectServiceImpl.java | 16 ++++----- .../service/impl/ProjectServiceImplTest.java | 34 +++++++++---------- 11 files changed, 97 insertions(+), 66 deletions(-) create mode 100644 core/src/main/java/org/opendevstack/apiservice/core/config/JacksonConfig.java delete mode 100644 service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/CreateProjectResponseMapper.java create mode 100644 service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/ProjectResponseMapper.java rename service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/{CreateProjectRequest.java => ProjectRequest.java} (90%) rename service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/{CreateProjectResponse.java => ProjectResponse.java} (62%) diff --git a/api-project/openapi/api-project.yaml b/api-project/openapi/api-project.yaml index 8fbd57b..3df5b3b 100644 --- a/api-project/openapi/api-project.yaml +++ b/api-project/openapi/api-project.yaml @@ -150,3 +150,5 @@ components: type: string errorDescription: type: string + location: + type: string diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java b/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java index f9b24c8..badf301 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java @@ -1,15 +1,34 @@ package org.opendevstack.apiservice.project.mapper; +import org.apache.logging.log4j.util.Strings; import org.mapstruct.Mapper; import org.opendevstack.apiservice.project.model.CreateProjectRequest; import org.opendevstack.apiservice.project.model.CreateProjectResponse; +import org.opendevstack.apiservice.serviceproject.model.ProjectRequest; +import org.opendevstack.apiservice.serviceproject.model.ProjectResponse; @Mapper(componentModel = "spring") public interface ProjectMapper { - org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest toServiceRequest( + ProjectRequest toServiceRequest( CreateProjectRequest apiRequest); - - CreateProjectResponse toApiResponse( - org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse serviceResponse); + + default CreateProjectResponse toApiResponse(ProjectResponse serviceResponse) { + if (serviceResponse == null) { + return null; + } + + CreateProjectResponse response = new CreateProjectResponse(); + response.setProjectKey(serviceResponse.getProjectKey()); + + if (!Strings.isEmpty(serviceResponse.getStatus())) { + response.setStatus(serviceResponse.getStatus()); + } + + if (!Strings.isEmpty(serviceResponse.getProjectKey())) { + response.setLocation("/api/pub/v0/projects/" + serviceResponse.getProjectKey()); + } + + return response; + } } diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java index dc0cf00..3d4c4ac 100644 --- a/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java @@ -9,6 +9,8 @@ import org.opendevstack.apiservice.project.mapper.ProjectMapper; import org.opendevstack.apiservice.project.model.CreateProjectRequest; import org.opendevstack.apiservice.project.model.CreateProjectResponse; +import org.opendevstack.apiservice.serviceproject.model.ProjectRequest; +import org.opendevstack.apiservice.serviceproject.model.ProjectResponse; import org.opendevstack.apiservice.serviceproject.service.ProjectService; import static org.assertj.core.api.Assertions.assertThat; @@ -35,13 +37,13 @@ void createProject_whenServiceReturnsValue_thenMapToApiModel() throws Exception CreateProjectRequest request = new CreateProjectRequest("My Project"); request.setProjectKey("PROJ01"); - org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse serviceResponse = - new org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse(); + ProjectResponse serviceResponse = + new ProjectResponse(); serviceResponse.setProjectKey("PROJ01"); serviceResponse.setStatus("Initiated"); when(projectService.createProject(org.mockito.ArgumentMatchers.any( - org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest.class))) + ProjectRequest.class))) .thenReturn(serviceResponse); CreateProjectResponse response = sut.createProject(request); @@ -50,14 +52,14 @@ void createProject_whenServiceReturnsValue_thenMapToApiModel() throws Exception assertThat(response.getProjectKey()).isEqualTo("PROJ01"); assertThat(response.getStatus()).isEqualTo("Initiated"); verify(projectService).createProject(org.mockito.ArgumentMatchers.any( - org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest.class)); + ProjectRequest.class)); } @Test void createProject_whenServiceReturnsNull_thenReturnNull() throws Exception { CreateProjectRequest request = new CreateProjectRequest("My Project"); when(projectService.createProject(org.mockito.ArgumentMatchers.any( - org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest.class))) + ProjectRequest.class))) .thenReturn(null); CreateProjectResponse response = sut.createProject(request); @@ -67,8 +69,8 @@ void createProject_whenServiceReturnsNull_thenReturnNull() throws Exception { @Test void getProject_whenServiceReturnsValue_thenMapToApiModel() throws Exception { - org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse serviceResponse = - new org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse(); + ProjectResponse serviceResponse = + new ProjectResponse(); serviceResponse.setProjectKey("PROJ01"); serviceResponse.setStatus("Found"); diff --git a/core/src/main/java/org/opendevstack/apiservice/core/config/JacksonConfig.java b/core/src/main/java/org/opendevstack/apiservice/core/config/JacksonConfig.java new file mode 100644 index 0000000..da25ff0 --- /dev/null +++ b/core/src/main/java/org/opendevstack/apiservice/core/config/JacksonConfig.java @@ -0,0 +1,21 @@ +package org.opendevstack.apiservice.core.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * Global Jackson configuration. + * Ensures that null properties are omitted from all JSON responses. + */ +@Configuration +public class JacksonConfig { + + @Bean + public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { + return builder -> builder.serializationInclusion(JsonInclude.Include.NON_NULL); + } +} + + diff --git a/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 deleted file mode 100644 index 7f187fd..0000000 --- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/CreateProjectResponseMapper.java +++ /dev/null @@ -1,16 +0,0 @@ -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/mapper/ProjectResponseMapper.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/ProjectResponseMapper.java new file mode 100644 index 0000000..7ebcfb0 --- /dev/null +++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/ProjectResponseMapper.java @@ -0,0 +1,11 @@ +package org.opendevstack.apiservice.serviceproject.mapper; + +import org.mapstruct.Mapper; +import org.opendevstack.apiservice.persistence.entity.ProjectEntity; +import org.opendevstack.apiservice.serviceproject.model.ProjectResponse; + +@Mapper(componentModel = "spring") +public interface ProjectResponseMapper { + + ProjectResponse toCreateProjectResponse(ProjectEntity entity); +} diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectRequest.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectRequest.java similarity index 90% rename from service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectRequest.java rename to service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectRequest.java index 22122b6..186ffaa 100644 --- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectRequest.java +++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectRequest.java @@ -7,7 +7,7 @@ @Data @NoArgsConstructor @AllArgsConstructor -public class CreateProjectRequest { +public class ProjectRequest { private String projectKey; diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectResponse.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectResponse.java similarity index 62% rename from service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectResponse.java rename to service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectResponse.java index 1dcd607..41f4b0a 100644 --- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/CreateProjectResponse.java +++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectResponse.java @@ -9,17 +9,9 @@ @NoArgsConstructor @AllArgsConstructor @Builder -public class CreateProjectResponse { +public class ProjectResponse { private String projectKey; private String status; - - private String message; - - private String error; - - private String errorKey; - - private String errorDescription; } diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java index 63f2d66..c2abcc6 100644 --- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java +++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java @@ -1,12 +1,12 @@ package org.opendevstack.apiservice.serviceproject.service; -import org.opendevstack.apiservice.serviceproject.model.CreateProjectRequest; -import org.opendevstack.apiservice.serviceproject.model.CreateProjectResponse; +import org.opendevstack.apiservice.serviceproject.model.ProjectRequest; +import org.opendevstack.apiservice.serviceproject.model.ProjectResponse; public interface ProjectService { - CreateProjectResponse createProject(CreateProjectRequest request); + ProjectResponse createProject(ProjectRequest request); - CreateProjectResponse getProject(String projectKey); + ProjectResponse getProject(String projectKey); } diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java index bee32a8..70232f2 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 @@ -7,9 +7,9 @@ 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.mapper.ProjectResponseMapper; +import org.opendevstack.apiservice.serviceproject.model.ProjectRequest; +import org.opendevstack.apiservice.serviceproject.model.ProjectResponse; import org.opendevstack.apiservice.serviceproject.service.GenerateProjectKeyService; import org.opendevstack.apiservice.serviceproject.service.ProjectService; import org.springframework.stereotype.Service; @@ -31,19 +31,19 @@ public class ProjectServiceImpl implements ProjectService { private final ProjectRepository projectRepository; - private final CreateProjectResponseMapper createProjectResponseMapper; + private final ProjectResponseMapper projectResponseMapper; @Override - public CreateProjectResponse createProject(CreateProjectRequest request) { - return CreateProjectResponse.builder().build(); + public ProjectResponse createProject(ProjectRequest request) { + return ProjectResponse.builder().build(); } @Override - public CreateProjectResponse getProject(String projectKey) { + public ProjectResponse getProject(String projectKey) { Optional project = projectRepository.findByProjectKey(projectKey); if (project.isPresent()) { - return createProjectResponseMapper.toCreateProjectResponse(project.get()); + return projectResponseMapper.toCreateProjectResponse(project.get()); } return null; diff --git a/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java b/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImplTest.java index e9db6ae..6b0c06e 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 @@ -9,9 +9,9 @@ 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.mapper.ProjectResponseMapper; +import org.opendevstack.apiservice.serviceproject.model.ProjectRequest; +import org.opendevstack.apiservice.serviceproject.model.ProjectResponse; import org.opendevstack.apiservice.serviceproject.service.GenerateProjectKeyService; import java.util.Optional; @@ -44,7 +44,7 @@ class ProjectServiceImplTest { private ProjectRepository projectRepository; @Mock - private CreateProjectResponseMapper createProjectResponseMapper; + private ProjectResponseMapper projectResponseMapper; private ProjectServiceImpl projectService; @@ -57,7 +57,7 @@ void setUp() { jiraService, generateProjectKeyService, projectRepository, - createProjectResponseMapper + projectResponseMapper ); } @@ -81,23 +81,23 @@ void get_project_returns_response_when_project_exists() { .ldapGroupTeam("cn=my-project-team,ou=groups,dc=example,dc=com") .build(); - CreateProjectResponse expectedResponse = CreateProjectResponse.builder() + ProjectResponse expectedResponse = ProjectResponse.builder() .projectKey(projectKey) .status("Completed") .build(); when(projectRepository.findByProjectKey(projectKey)).thenReturn(Optional.of(projectEntity)); - when(createProjectResponseMapper.toCreateProjectResponse(projectEntity)).thenReturn(expectedResponse); + when(projectResponseMapper.toCreateProjectResponse(projectEntity)).thenReturn(expectedResponse); // WHEN - CreateProjectResponse result = projectService.getProject(projectKey); + ProjectResponse result = projectService.getProject(projectKey); // THEN assertNotNull(result); assertEquals(projectKey, result.getProjectKey()); assertEquals("Completed", result.getStatus()); verify(projectRepository).findByProjectKey(projectKey); - verify(createProjectResponseMapper).toCreateProjectResponse(projectEntity); + verify(projectResponseMapper).toCreateProjectResponse(projectEntity); } @Test @@ -108,25 +108,25 @@ void get_project_returns_null_when_project_does_not_exist() { when(projectRepository.findByProjectKey(projectKey)).thenReturn(Optional.empty()); // WHEN - CreateProjectResponse result = projectService.getProject(projectKey); + ProjectResponse result = projectService.getProject(projectKey); // THEN assertNull(result); verify(projectRepository).findByProjectKey(projectKey); - verify(createProjectResponseMapper, never()).toCreateProjectResponse(any()); + verify(projectResponseMapper, never()).toCreateProjectResponse(any()); } @Test void create_project_returns_empty_response() { // GIVEN - CreateProjectRequest request = new CreateProjectRequest(); + ProjectRequest request = new ProjectRequest(); request.setProjectKey("NEW-PROJECT"); request.setProjectKeyPattern("NEW%06d"); request.setProjectName("New Project"); request.setProjectDescription("New test project"); // WHEN - CreateProjectResponse result = projectService.createProject(request); + ProjectResponse result = projectService.createProject(request); // THEN assertNotNull(result); @@ -152,7 +152,7 @@ void get_project_returns_null_when_project_key_is_null() { when(projectRepository.findByProjectKey(null)).thenReturn(Optional.empty()); // WHEN - CreateProjectResponse result = projectService.getProject(null); + ProjectResponse result = projectService.getProject(null); // THEN assertNull(result); @@ -173,16 +173,16 @@ void get_project_returns_response_for_soft_deleted_project() { .deleted(true) .build(); - CreateProjectResponse expectedResponse = CreateProjectResponse.builder() + ProjectResponse expectedResponse = ProjectResponse.builder() .projectKey(projectKey) .status("Deleted") .build(); when(projectRepository.findByProjectKey(projectKey)).thenReturn(Optional.of(deletedEntity)); - when(createProjectResponseMapper.toCreateProjectResponse(deletedEntity)).thenReturn(expectedResponse); + when(projectResponseMapper.toCreateProjectResponse(deletedEntity)).thenReturn(expectedResponse); // WHEN - CreateProjectResponse result = projectService.getProject(projectKey); + ProjectResponse result = projectService.getProject(projectKey); // THEN assertNotNull(result); From 40af2da33a6473461814f5576f6c93516cf50347 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Tue, 17 Mar 2026 16:56:48 +0100 Subject: [PATCH 16/20] Add projectFlavor field to response models, update error handling, and refactor repository methods for case-insensitive project key lookup --- api-project/openapi/api-project.yaml | 2 + .../project/controller/ProjectController.java | 4 + .../controller/ProjectResponseFactory.java | 2 +- .../project/mapper/ProjectMapper.java | 36 ++-- .../ProjectControllerIntegrationTest.java | 87 --------- .../controller/ProjectControllerTest.java | 16 +- .../facade/impl/ProjectsFacadeImplTest.java | 9 +- .../project/mapper/ProjectMapperTest.java | 176 ++++++++++++++++++ .../persistence/entity/ProjectEntity.java | 3 +- .../repository/ProjectRepository.java | 25 +-- .../ProjectRepositoryIntegrationTest.java | 28 ++- .../repository/ProjectRepositoryTest.java | 14 +- .../mapper/ProjectResponseMapper.java | 20 +- .../serviceproject/model/ProjectResponse.java | 4 +- .../serviceproject/model/Status.java | 15 ++ .../service/impl/ProjectServiceImpl.java | 2 +- .../service/impl/ProjectServiceImplTest.java | 60 +++--- 17 files changed, 307 insertions(+), 196 deletions(-) delete mode 100644 api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerIntegrationTest.java create mode 100644 api-project/src/test/java/org/opendevstack/apiservice/project/mapper/ProjectMapperTest.java create mode 100644 service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/Status.java diff --git a/api-project/openapi/api-project.yaml b/api-project/openapi/api-project.yaml index 3df5b3b..9c83d30 100644 --- a/api-project/openapi/api-project.yaml +++ b/api-project/openapi/api-project.yaml @@ -142,6 +142,8 @@ components: type: string status: type: string + projectFlavor: + type: string message: type: string error: diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java index a17d338..0d63692 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java @@ -58,6 +58,10 @@ public ResponseEntity getProject(@PathVariable String pro log.error("Error retrieving project '{}': {}", projectKey, e.getMessage(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(ProjectResponseFactory.internalError()); + } catch (Exception e) { + log.error("Unexpected error retrieving project '{}': {}", projectKey, e.getMessage(), e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ProjectResponseFactory.internalError()); } } } diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java index 6a6faae..4de1a6e 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java @@ -32,7 +32,7 @@ private static CreateProjectResponse error(String error, String errorKey, String CreateProjectResponse response = new CreateProjectResponse(); response.setError(error); response.setErrorKey(errorKey); - response.setMessage(message); + response.setErrorDescription(message); return response; } } \ No newline at end of file diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java b/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java index badf301..131f840 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java @@ -1,34 +1,36 @@ package org.opendevstack.apiservice.project.mapper; -import org.apache.logging.log4j.util.Strings; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; import org.opendevstack.apiservice.project.model.CreateProjectRequest; import org.opendevstack.apiservice.project.model.CreateProjectResponse; import org.opendevstack.apiservice.serviceproject.model.ProjectRequest; import org.opendevstack.apiservice.serviceproject.model.ProjectResponse; +import org.opendevstack.apiservice.serviceproject.model.Status; @Mapper(componentModel = "spring") public interface ProjectMapper { - - ProjectRequest toServiceRequest( - CreateProjectRequest apiRequest); - default CreateProjectResponse toApiResponse(ProjectResponse serviceResponse) { - if (serviceResponse == null) { - return null; - } + ProjectRequest toServiceRequest(CreateProjectRequest apiRequest); - CreateProjectResponse response = new CreateProjectResponse(); - response.setProjectKey(serviceResponse.getProjectKey()); - - if (!Strings.isEmpty(serviceResponse.getStatus())) { - response.setStatus(serviceResponse.getStatus()); - } + @Mapping(source = "status", target = "status", qualifiedByName = "mapStatus") + @Mapping(source = "projectKey", target = "location", qualifiedByName = "mapLocation") + CreateProjectResponse toApiResponse(ProjectResponse serviceResponse); - if (!Strings.isEmpty(serviceResponse.getProjectKey())) { - response.setLocation("/api/pub/v0/projects/" + serviceResponse.getProjectKey()); + @Named("mapStatus") + default String mapStatus(Status status) { + if (status == null) { + return null; } + return status.getDbValue(); + } - return response; + @Named("mapLocation") + default String mapLocation(String projectKey) { + if (projectKey == null || projectKey.isEmpty()) { + return null; } + return "/api/pub/v0/projects/" + projectKey; + } } diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerIntegrationTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerIntegrationTest.java deleted file mode 100644 index 2ee32d5..0000000 --- a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerIntegrationTest.java +++ /dev/null @@ -1,87 +0,0 @@ -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 9e73084..d5d5c84 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 @@ -70,10 +70,10 @@ void createProject_whenProjectCreationException_thenReturnConflict() throws Exce assertThat(result.getBody()).isNotNull(); assertThat(result.getBody().getError()).isEqualTo("CONFLICT"); assertThat(result.getBody().getErrorKey()).isEqualTo("PROJECT_ALREADY_EXISTS"); - assertThat(result.getBody().getMessage()).contains("already exists"); + assertThat(result.getBody().getErrorDescription()).contains("Project with key 'EXISTING' already exists"); assertThat(result.getBody().getProjectKey()).isNull(); assertThat(result.getBody().getStatus()).isNull(); - assertThat(result.getBody().getErrorDescription()).isNull(); + assertThat(result.getBody().getMessage()).isNull(); } @Test @@ -89,10 +89,10 @@ void createProject_whenProjectKeyGenerationException_thenReturnInternalServerErr assertThat(result.getBody()).isNotNull(); 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().getErrorDescription()).isEqualTo("Failed to generate a unique project key."); assertThat(result.getBody().getProjectKey()).isNull(); assertThat(result.getBody().getStatus()).isNull(); - assertThat(result.getBody().getErrorDescription()).isNull(); + assertThat(result.getBody().getMessage()).isNull(); } @Test @@ -123,10 +123,10 @@ void getProject_whenNotFound_thenReturnNotFound() throws Exception { assertThat(result.getBody()).isNotNull(); assertThat(result.getBody().getError()).isEqualTo("NOT_FOUND"); assertThat(result.getBody().getErrorKey()).isEqualTo("PROJECT_NOT_FOUND"); - assertThat(result.getBody().getMessage()).contains("UNKNOWN"); + assertThat(result.getBody().getErrorDescription()).contains("UNKNOWN"); assertThat(result.getBody().getProjectKey()).isNull(); assertThat(result.getBody().getStatus()).isNull(); - assertThat(result.getBody().getErrorDescription()).isNull(); + assertThat(result.getBody().getMessage()).isNull(); } @Test @@ -140,10 +140,10 @@ void getProject_whenServiceThrows_thenReturnInternalServerError() throws Excepti assertThat(result.getBody()).isNotNull(); 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().getErrorDescription()).isEqualTo("An error occurred while processing the request."); assertThat(result.getBody().getProjectKey()).isNull(); assertThat(result.getBody().getStatus()).isNull(); - assertThat(result.getBody().getErrorDescription()).isNull(); + assertThat(result.getBody().getMessage()).isNull(); } } diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java index 3d4c4ac..c11a596 100644 --- a/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImplTest.java @@ -11,6 +11,7 @@ import org.opendevstack.apiservice.project.model.CreateProjectResponse; import org.opendevstack.apiservice.serviceproject.model.ProjectRequest; import org.opendevstack.apiservice.serviceproject.model.ProjectResponse; +import org.opendevstack.apiservice.serviceproject.model.Status; import org.opendevstack.apiservice.serviceproject.service.ProjectService; import static org.assertj.core.api.Assertions.assertThat; @@ -40,7 +41,7 @@ void createProject_whenServiceReturnsValue_thenMapToApiModel() throws Exception ProjectResponse serviceResponse = new ProjectResponse(); serviceResponse.setProjectKey("PROJ01"); - serviceResponse.setStatus("Initiated"); + serviceResponse.setStatus(Status.PENDING); when(projectService.createProject(org.mockito.ArgumentMatchers.any( ProjectRequest.class))) @@ -50,7 +51,7 @@ void createProject_whenServiceReturnsValue_thenMapToApiModel() throws Exception assertThat(response).isNotNull(); assertThat(response.getProjectKey()).isEqualTo("PROJ01"); - assertThat(response.getStatus()).isEqualTo("Initiated"); + assertThat(response.getStatus()).isEqualTo("Pending"); verify(projectService).createProject(org.mockito.ArgumentMatchers.any( ProjectRequest.class)); } @@ -72,7 +73,7 @@ void getProject_whenServiceReturnsValue_thenMapToApiModel() throws Exception { ProjectResponse serviceResponse = new ProjectResponse(); serviceResponse.setProjectKey("PROJ01"); - serviceResponse.setStatus("Found"); + serviceResponse.setStatus(Status.RUNNING); when(projectService.getProject("PROJ01")).thenReturn(serviceResponse); @@ -80,7 +81,7 @@ void getProject_whenServiceReturnsValue_thenMapToApiModel() throws Exception { assertThat(response).isNotNull(); assertThat(response.getProjectKey()).isEqualTo("PROJ01"); - assertThat(response.getStatus()).isEqualTo("Found"); + assertThat(response.getStatus()).isEqualTo("Running"); } @Test diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/mapper/ProjectMapperTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/mapper/ProjectMapperTest.java new file mode 100644 index 0000000..7229bf0 --- /dev/null +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/mapper/ProjectMapperTest.java @@ -0,0 +1,176 @@ +package org.opendevstack.apiservice.project.mapper; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mapstruct.factory.Mappers; +import org.mockito.MockitoAnnotations; +import org.opendevstack.apiservice.project.model.CreateProjectRequest; +import org.opendevstack.apiservice.project.model.CreateProjectResponse; +import org.opendevstack.apiservice.serviceproject.model.ProjectRequest; +import org.opendevstack.apiservice.serviceproject.model.ProjectResponse; +import org.opendevstack.apiservice.serviceproject.model.Status; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +class ProjectMapperTest { + + private ProjectMapper projectMapper; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + projectMapper = Mappers.getMapper(ProjectMapper.class); + } + + @Test + void to_service_request_maps_all_fields_correctly() { + CreateProjectRequest apiRequest = new CreateProjectRequest("My Project"); + apiRequest.setProjectKey("PROJ01"); + apiRequest.setProjectKeyPattern("SS%06d"); + apiRequest.setProjectDescription("A test project"); + + ProjectRequest result = projectMapper.toServiceRequest(apiRequest); + + assertNotNull(result); + assertEquals("PROJ01", result.getProjectKey()); + assertEquals("SS%06d", result.getProjectKeyPattern()); + assertEquals("My Project", result.getProjectName()); + assertEquals("A test project", result.getProjectDescription()); + } + + @Test + void to_service_request_returns_null_when_input_is_null() { + CreateProjectRequest apiRequest = null; + + ProjectRequest result = projectMapper.toServiceRequest(apiRequest); + + assertNull(result); + } + + @Test + void to_service_request_maps_only_required_field() { + CreateProjectRequest apiRequest = new CreateProjectRequest("Only Name"); + + ProjectRequest result = projectMapper.toServiceRequest(apiRequest); + + assertNotNull(result); + assertNull(result.getProjectKey()); + assertNull(result.getProjectKeyPattern()); + assertEquals("Only Name", result.getProjectName()); + assertNull(result.getProjectDescription()); + } + + @Test + void to_api_response_maps_all_fields_correctly() { + ProjectResponse serviceResponse = ProjectResponse.builder() + .projectKey("PROJ01") + .status(Status.PENDING) + .projectFlavor("AMP") + .build(); + + CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse); + + assertNotNull(result); + assertEquals("PROJ01", result.getProjectKey()); + assertEquals("Pending", result.getStatus()); + assertEquals("AMP", result.getProjectFlavor()); + assertEquals("/api/pub/v0/projects/PROJ01", result.getLocation()); + } + + @Test + void to_api_response_returns_null_when_input_is_null() { + ProjectResponse serviceResponse = null; + + CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse); + + assertNull(result); + } + + @Test + void to_api_response_maps_with_null_status() { + ProjectResponse serviceResponse = ProjectResponse.builder() + .projectKey("PROJ02") + .status(null) + .build(); + + CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse); + + assertNotNull(result); + assertEquals("PROJ02", result.getProjectKey()); + assertNull(result.getStatus()); + assertEquals("/api/pub/v0/projects/PROJ02", result.getLocation()); + } + + @Test + void to_api_response_does_not_set_location_when_project_key_is_null() { + ProjectResponse serviceResponse = ProjectResponse.builder() + .projectKey(null) + .status(Status.RUNNING) + .build(); + + CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse); + + assertNotNull(result); + assertNull(result.getProjectKey()); + assertEquals("Running", result.getStatus()); + assertNull(result.getLocation()); + } + + @Test + void to_api_response_does_not_set_location_when_project_key_is_empty() { + ProjectResponse serviceResponse = ProjectResponse.builder() + .projectKey("") + .status(Status.FAILED) + .build(); + + CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse); + + assertNotNull(result); + assertEquals("", result.getProjectKey()); + assertEquals("Failed", result.getStatus()); + assertNull(result.getLocation()); + } + + @Test + void to_api_response_maps_status_running_to_db_value() { + ProjectResponse serviceResponse = ProjectResponse.builder() + .projectKey("RUN01") + .status(Status.RUNNING) + .build(); + + CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse); + + assertNotNull(result); + assertEquals("Running", result.getStatus()); + } + + @Test + void to_api_response_maps_status_failed_to_db_value() { + ProjectResponse serviceResponse = ProjectResponse.builder() + .projectKey("FAIL01") + .status(Status.FAILED) + .build(); + + CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse); + + assertNotNull(result); + assertEquals("Failed", result.getStatus()); + } + + @Test + void to_api_response_maps_null_project_flavor() { + ProjectResponse serviceResponse = ProjectResponse.builder() + .projectKey("PROJ03") + .status(Status.PENDING) + .projectFlavor(null) + .build(); + + CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse); + + assertNotNull(result); + assertEquals("PROJ03", result.getProjectKey()); + assertNull(result.getProjectFlavor()); + } +} diff --git a/persistence/src/main/java/org/opendevstack/apiservice/persistence/entity/ProjectEntity.java b/persistence/src/main/java/org/opendevstack/apiservice/persistence/entity/ProjectEntity.java index 635a1e7..f3299cb 100644 --- a/persistence/src/main/java/org/opendevstack/apiservice/persistence/entity/ProjectEntity.java +++ b/persistence/src/main/java/org/opendevstack/apiservice/persistence/entity/ProjectEntity.java @@ -78,7 +78,8 @@ public class ProjectEntity { private String projectFlavor; /** - * Provisioning status. Known values: {@code Failed}, {@code Pending} (null = completed). + * Provisioning status. Known values: {@code Pending}, {@code Running}, {@code Failed} + * ({@code null} = completed successfully). */ @Column(name = "status", length = 50) private String status; diff --git a/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/ProjectRepository.java b/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/ProjectRepository.java index 68f344c..2a5dc00 100644 --- a/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/ProjectRepository.java +++ b/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/ProjectRepository.java @@ -20,26 +20,11 @@ */ @Repository public interface ProjectRepository extends JpaRepository { - - /** - * Finds a project by its unique business key regardless of soft-delete status. Use - * this when you explicitly need to handle deleted projects (e.g. restore flows). - * @param projectKey the Atlassian-style project key - * @return the matching project, or {@link Optional#empty()} if it has never existed - */ - Optional findByProjectKey(String projectKey); - - /** - * Returns all active (non-deleted) projects. - * @return list of active projects; empty list if none exist - */ + + Optional findByProjectKeyIgnoreCase(String projectKey); + List findByDeletedFalse(); - - /** - * Checks whether an active project with the given key already exists. - * @param projectKey the project key to look up - * @return {@code true} if an active project with this key exists - */ - boolean existsByProjectKey(String projectKey); + + boolean existsByProjectKeyIgnoreCase(String projectKey); } diff --git a/persistence/src/test/java/org/opendevstack/apiservice/persistence/integration/ProjectRepositoryIntegrationTest.java b/persistence/src/test/java/org/opendevstack/apiservice/persistence/integration/ProjectRepositoryIntegrationTest.java index be517a6..b944e73 100644 --- a/persistence/src/test/java/org/opendevstack/apiservice/persistence/integration/ProjectRepositoryIntegrationTest.java +++ b/persistence/src/test/java/org/opendevstack/apiservice/persistence/integration/ProjectRepositoryIntegrationTest.java @@ -113,19 +113,17 @@ void tearDown() { } private void cleanupTestData() { - TEST_KEYS.forEach(key -> projectRepository.findByProjectKey(key).ifPresent(projectRepository::delete)); + TEST_KEYS.forEach(key -> projectRepository.findByProjectKeyIgnoreCase(key).ifPresent(projectRepository::delete)); } - // ── findByProjectKey ────────────────────────────────────────────────────── - @Nested - @DisplayName("findByProjectKey") - class FindByProjectKey { + @DisplayName("findByProjectKeyIgnoreCase") + class findByProjectKeyIgnoreCase { @Test @DisplayName("returns active project by its key") void returnsActiveProjectByKey() { - Optional result = projectRepository.findByProjectKey("IT-ACTIVE-01"); + Optional result = projectRepository.findByProjectKeyIgnoreCase("IT-ACTIVE-01"); assertThat(result).isPresent(); assertThat(result.get().getProjectKey()).isEqualTo("IT-ACTIVE-01"); @@ -135,7 +133,7 @@ void returnsActiveProjectByKey() { @Test @DisplayName("returns soft-deleted project by its key") void returnsDeletedProjectByKey() { - Optional result = projectRepository.findByProjectKey("IT-DELETED-01"); + Optional result = projectRepository.findByProjectKeyIgnoreCase("IT-DELETED-01"); assertThat(result).isPresent(); assertThat(result.get().getProjectKey()).isEqualTo("IT-DELETED-01"); @@ -145,7 +143,7 @@ void returnsDeletedProjectByKey() { @Test @DisplayName("returns empty Optional for a key that has never existed") void returnsEmptyForUnknownKey() { - Optional result = projectRepository.findByProjectKey("NONEXISTENTKEY"); + Optional result = projectRepository.findByProjectKeyIgnoreCase("NONEXISTENTKEY"); assertThat(result).isEmpty(); } @@ -153,7 +151,7 @@ void returnsEmptyForUnknownKey() { @Test @DisplayName("returned entity contains all persisted fields") void returnedEntityContainsAllFields() { - Optional result = projectRepository.findByProjectKey("IT-DELETED-01"); + Optional result = projectRepository.findByProjectKeyIgnoreCase("IT-DELETED-01"); assertThat(result).isPresent(); ProjectEntity entity = result.get(); @@ -164,8 +162,6 @@ void returnedEntityContainsAllFields() { } } - // ── findByDeletedFalse ──────────────────────────────────────────────────── - @Nested @DisplayName("findByDeletedFalse") class FindByDeletedFalse { @@ -212,24 +208,22 @@ class ExistsByProjectKey { @Test @DisplayName("returns true for an existing active project key") void returnsTrueForActiveKey() { - assertThat(projectRepository.existsByProjectKey("IT-ACTIVE-01")).isTrue(); + assertThat(projectRepository.existsByProjectKeyIgnoreCase("IT-ACTIVE-01")).isTrue(); } @Test @DisplayName("returns true for a soft-deleted project key") void returnsTrueForDeletedKey() { - assertThat(projectRepository.existsByProjectKey("IT-DELETED-01")).isTrue(); + assertThat(projectRepository.existsByProjectKeyIgnoreCase("IT-DELETED-01")).isTrue(); } @Test @DisplayName("returns false for a key that has never existed") void returnsFalseForNonExistentKey() { - assertThat(projectRepository.existsByProjectKey("NONEXISTENTKEY")).isFalse(); + assertThat(projectRepository.existsByProjectKeyIgnoreCase("NONEXISTENTKEY")).isFalse(); } } - - // ── Persistence lifecycle ───────────────────────────────────────────────── - + @Nested @DisplayName("Persistence lifecycle") class PersistenceLifecycle { diff --git a/persistence/src/test/java/org/opendevstack/apiservice/persistence/repository/ProjectRepositoryTest.java b/persistence/src/test/java/org/opendevstack/apiservice/persistence/repository/ProjectRepositoryTest.java index e067117..ca30d90 100644 --- a/persistence/src/test/java/org/opendevstack/apiservice/persistence/repository/ProjectRepositoryTest.java +++ b/persistence/src/test/java/org/opendevstack/apiservice/persistence/repository/ProjectRepositoryTest.java @@ -123,24 +123,22 @@ void save_duplicateProjectKey_throwsException() { } } - - // ── findByProjectKey ────────────────────────────────────────────────────── - + @Nested - @DisplayName("findByProjectKey") - class FindByProjectKey { + @DisplayName("findByProjectKeyIgnoreCase") + class findByProjectKeyIgnoreCase { @Test @DisplayName("returns project regardless of deleted flag") void returnsProjectRegardlessOfDeletedFlag() { - assertThat(repository.findByProjectKey("ACTIVE-01")).isPresent(); - assertThat(repository.findByProjectKey("DELETED-01")).isPresent(); + assertThat(repository.findByProjectKeyIgnoreCase("ACTIVE-01")).isPresent(); + assertThat(repository.findByProjectKeyIgnoreCase("DELETED-01")).isPresent(); } @Test @DisplayName("returns empty for unknown key") void returnsEmptyForUnknownKey() { - assertThat(repository.findByProjectKey("UNKNOWN")).isEmpty(); + assertThat(repository.findByProjectKeyIgnoreCase("UNKNOWN")).isEmpty(); } } diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/ProjectResponseMapper.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/ProjectResponseMapper.java index 7ebcfb0..18a6491 100644 --- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/ProjectResponseMapper.java +++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/ProjectResponseMapper.java @@ -1,11 +1,29 @@ package org.opendevstack.apiservice.serviceproject.mapper; +import java.util.Arrays; import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.Named; import org.opendevstack.apiservice.persistence.entity.ProjectEntity; import org.opendevstack.apiservice.serviceproject.model.ProjectResponse; +import org.opendevstack.apiservice.serviceproject.model.Status; @Mapper(componentModel = "spring") public interface ProjectResponseMapper { - + + @Mapping(source = "status", target = "status", qualifiedByName = "mapStatus") ProjectResponse toCreateProjectResponse(ProjectEntity entity); + + @Named("mapStatus") + default Status mapStatus(String value) { + if (value == null) { + return null; + } + + return Arrays.stream(Status.values()) + .filter(s -> s.getDbValue().equalsIgnoreCase(value)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException( + "Unknown Status db value: '" + value + "'")); + } } diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectResponse.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectResponse.java index 41f4b0a..40ee62a 100644 --- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectResponse.java +++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectResponse.java @@ -13,5 +13,7 @@ public class ProjectResponse { private String projectKey; - private String status; + private Status status; + + private String projectFlavor; } diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/Status.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/Status.java new file mode 100644 index 0000000..960efc6 --- /dev/null +++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/Status.java @@ -0,0 +1,15 @@ +package org.opendevstack.apiservice.serviceproject.model; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Status { + + PENDING("Pending"), + RUNNING("Running"), + FAILED("Failed"); + + private final String dbValue; +} diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java index 70232f2..1d8d7b6 100644 --- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java +++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectServiceImpl.java @@ -40,7 +40,7 @@ public ProjectResponse createProject(ProjectRequest request) { @Override public ProjectResponse getProject(String projectKey) { - Optional project = projectRepository.findByProjectKey(projectKey); + Optional project = projectRepository.findByProjectKeyIgnoreCase(projectKey); if (project.isPresent()) { return projectResponseMapper.toCreateProjectResponse(project.get()); 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 6b0c06e..827ba41 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 @@ -12,6 +12,7 @@ import org.opendevstack.apiservice.serviceproject.mapper.ProjectResponseMapper; import org.opendevstack.apiservice.serviceproject.model.ProjectRequest; import org.opendevstack.apiservice.serviceproject.model.ProjectResponse; +import org.opendevstack.apiservice.serviceproject.model.Status; import org.opendevstack.apiservice.serviceproject.service.GenerateProjectKeyService; import java.util.Optional; @@ -63,7 +64,7 @@ void setUp() { @Test void get_project_returns_response_when_project_exists() { - // GIVEN + String projectKey = "MY-PROJECT"; UUID projectId = UUID.randomUUID(); @@ -83,85 +84,84 @@ void get_project_returns_response_when_project_exists() { ProjectResponse expectedResponse = ProjectResponse.builder() .projectKey(projectKey) - .status("Completed") + .status(Status.RUNNING) .build(); - when(projectRepository.findByProjectKey(projectKey)).thenReturn(Optional.of(projectEntity)); + when(projectRepository.findByProjectKeyIgnoreCase(projectKey)).thenReturn(Optional.of(projectEntity)); when(projectResponseMapper.toCreateProjectResponse(projectEntity)).thenReturn(expectedResponse); - // WHEN + ProjectResponse result = projectService.getProject(projectKey); - // THEN + assertNotNull(result); assertEquals(projectKey, result.getProjectKey()); - assertEquals("Completed", result.getStatus()); - verify(projectRepository).findByProjectKey(projectKey); + assertEquals(Status.RUNNING, result.getStatus()); + verify(projectRepository).findByProjectKeyIgnoreCase(projectKey); verify(projectResponseMapper).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(projectRepository.findByProjectKeyIgnoreCase(projectKey)).thenReturn(Optional.empty()); - // WHEN + ProjectResponse result = projectService.getProject(projectKey); - // THEN + assertNull(result); - verify(projectRepository).findByProjectKey(projectKey); + verify(projectRepository).findByProjectKeyIgnoreCase(projectKey); verify(projectResponseMapper, never()).toCreateProjectResponse(any()); } @Test void create_project_returns_empty_response() { - // GIVEN + ProjectRequest request = new ProjectRequest(); request.setProjectKey("NEW-PROJECT"); request.setProjectKeyPattern("NEW%06d"); request.setProjectName("New Project"); request.setProjectDescription("New test project"); - // WHEN + ProjectResponse 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)) + when(projectRepository.findByProjectKeyIgnoreCase(projectKey)) .thenThrow(new RuntimeException("Database connection error")); - // WHEN / THEN assertThrows(RuntimeException.class, () -> projectService.getProject(projectKey)); - verify(projectRepository).findByProjectKey(projectKey); + verify(projectRepository).findByProjectKeyIgnoreCase(projectKey); } @Test void get_project_returns_null_when_project_key_is_null() { - // GIVEN - when(projectRepository.findByProjectKey(null)).thenReturn(Optional.empty()); + + when(projectRepository.findByProjectKeyIgnoreCase(null)).thenReturn(Optional.empty()); - // WHEN + ProjectResponse result = projectService.getProject(null); - // THEN + assertNull(result); - verify(projectRepository).findByProjectKey(null); + verify(projectRepository).findByProjectKeyIgnoreCase(null); } @Test void get_project_returns_response_for_soft_deleted_project() { - // GIVEN + String projectKey = "DELETED-PROJECT"; ProjectEntity deletedEntity = ProjectEntity.builder() @@ -175,18 +175,18 @@ void get_project_returns_response_for_soft_deleted_project() { ProjectResponse expectedResponse = ProjectResponse.builder() .projectKey(projectKey) - .status("Deleted") + .status(Status.RUNNING) .build(); - when(projectRepository.findByProjectKey(projectKey)).thenReturn(Optional.of(deletedEntity)); + when(projectRepository.findByProjectKeyIgnoreCase(projectKey)).thenReturn(Optional.of(deletedEntity)); when(projectResponseMapper.toCreateProjectResponse(deletedEntity)).thenReturn(expectedResponse); - // WHEN + ProjectResponse result = projectService.getProject(projectKey); - // THEN + assertNotNull(result); assertEquals(projectKey, result.getProjectKey()); - verify(projectRepository).findByProjectKey(projectKey); + verify(projectRepository).findByProjectKeyIgnoreCase(projectKey); } } \ No newline at end of file From 1f4441c8289d9c67468a3cecf10214698d026b6f Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Wed, 18 Mar 2026 13:45:27 +0100 Subject: [PATCH 17/20] Refactor project response handling to include location header and improve error messages in ProjectController and ProjectResponseFactory --- .../project/controller/ProjectController.java | 29 ++++++++----- .../controller/ProjectResponseFactory.java | 41 ++++++++++++------- .../project/facade/ProjectsFacade.java | 2 +- .../facade/impl/ProjectsFacadeImpl.java | 2 +- .../project/mapper/ProjectMapper.java | 13 ++++++ .../controller/ProjectControllerTest.java | 32 +++++++-------- .../project/mapper/ProjectMapperTest.java | 31 ++++++++++++++ 7 files changed, 107 insertions(+), 43 deletions(-) diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java index 0d63692..fe8012d 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectController.java @@ -26,42 +26,51 @@ public class ProjectController implements ProjectsApi { public static final String API_BASE_PATH = "/api/pub/v0/projects"; + private static final String HTTP_HEADER_LOCATION = "Location"; + private final ProjectsFacade projectsFacade; @PostMapping @Override public ResponseEntity createProject(@Valid @RequestBody CreateProjectRequest createProjectRequest) { try { - return ResponseEntity.ok(projectsFacade.createProject(createProjectRequest)); + return ResponseEntity + .status(HttpStatus.OK) + .header(HTTP_HEADER_LOCATION, API_BASE_PATH) + .body(projectsFacade.createProject(createProjectRequest)); } catch (ProjectCreationException e) { log.error("Project creation conflict: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.CONFLICT) - .body(ProjectResponseFactory.conflict(e.getMessage())); + .header(HTTP_HEADER_LOCATION, API_BASE_PATH) + .body(ProjectResponseFactory.conflict(e.getMessage(), API_BASE_PATH)); } catch (ProjectKeyGenerationException e) { log.error("Failed to generate project key: {}", e.getMessage(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ProjectResponseFactory.projectKeyGenerationFailed()); + .header(HTTP_HEADER_LOCATION, API_BASE_PATH) + .body(ProjectResponseFactory.projectKeyGenerationFailed(API_BASE_PATH)); } } @GetMapping("/{projectKey}") @Override public ResponseEntity getProject(@PathVariable String projectKey) { + String location = API_BASE_PATH + "/" + projectKey; try { CreateProjectResponse response = projectsFacade.getProject(projectKey); if (response == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(ProjectResponseFactory.notFound(projectKey)); + .header(HTTP_HEADER_LOCATION, location) + .body(ProjectResponseFactory.notFound(projectKey, location)); } - return ResponseEntity.ok(response); - } catch (ProjectCreationException e) { - log.error("Error retrieving project '{}': {}", projectKey, e.getMessage(), e); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ProjectResponseFactory.internalError()); + return ResponseEntity + .status(HttpStatus.OK) + .header(HTTP_HEADER_LOCATION, location) + .body(response); } catch (Exception e) { log.error("Unexpected error retrieving project '{}': {}", projectKey, e.getMessage(), e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) - .body(ProjectResponseFactory.internalError()); + .header(HTTP_HEADER_LOCATION, location) + .body(ProjectResponseFactory.internalError(location)); } } } diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java index 4de1a6e..3fe80e9 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java @@ -4,35 +4,46 @@ public final class ProjectResponseFactory { - private static final String INTERNAL_ERROR = "INTERNAL_ERROR"; - private ProjectResponseFactory() { } - public static CreateProjectResponse conflict(String message) { - return error("CONFLICT", "PROJECT_ALREADY_EXISTS", message); + public static CreateProjectResponse conflict(String message, String location) { + return error( + ErrorKey.PROJECT_ALREADY_EXISTS.getMessage(), + ErrorKey.PROJECT_ALREADY_EXISTS.getKey(), + message, location); } - public static CreateProjectResponse projectKeyGenerationFailed() { - return error(INTERNAL_ERROR, "PROJECT_KEY_GENERATION_FAILED", - "Failed to generate a unique project key."); + public static CreateProjectResponse projectKeyGenerationFailed(String location) { + return error(ErrorKey.INTERNAL_ERROR.getMessage(), + "PROJECT_KEY_GENERATION_FAILED", + "Failed to generate a unique project key.", + location); } - public static CreateProjectResponse notFound(String projectKey) { - return error("NOT_FOUND", "PROJECT_NOT_FOUND", - String.format("Project with key '%s' not found", projectKey)); + public static CreateProjectResponse notFound(String projectKey, String location) { + return error( + ErrorKey.PROJECT_NOT_FOUND.getMessage(), + ErrorKey.PROJECT_NOT_FOUND.getKey(), + String.format("Project with key '%s' not found", projectKey), + location + ); } - public static CreateProjectResponse internalError() { - return error(INTERNAL_ERROR, INTERNAL_ERROR, - "An error occurred while processing the request."); + public static CreateProjectResponse internalError(String location) { + return error( + ErrorKey.INTERNAL_ERROR.getMessage(), + ErrorKey.INTERNAL_ERROR.getKey(), + "An error occurred while processing the request.", + location); } - private static CreateProjectResponse error(String error, String errorKey, String message) { + private static CreateProjectResponse error(String error, String errorKey, String message, String location) { CreateProjectResponse response = new CreateProjectResponse(); response.setError(error); response.setErrorKey(errorKey); - response.setErrorDescription(message); + response.setMessage(message); + response.setLocation(location); return response; } } \ No newline at end of file diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/facade/ProjectsFacade.java b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/ProjectsFacade.java index 9d29f13..b30ad4f 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/facade/ProjectsFacade.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/ProjectsFacade.java @@ -10,5 +10,5 @@ public interface ProjectsFacade { CreateProjectResponse createProject(CreateProjectRequest request) throws ProjectCreationException, ProjectKeyGenerationException; - CreateProjectResponse getProject(String projectKey) throws ProjectCreationException; + CreateProjectResponse getProject(String projectKey); } \ No newline at end of file diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImpl.java b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImpl.java index 274ad1d..ea66ee8 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImpl.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectsFacadeImpl.java @@ -29,7 +29,7 @@ public CreateProjectResponse createProject(CreateProjectRequest request) } @Override - public CreateProjectResponse getProject(String projectKey) throws ProjectCreationException { + public CreateProjectResponse getProject(String projectKey) { return projectMapper.toApiResponse(projectService.getProject(projectKey)); } } \ No newline at end of file diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java b/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java index 131f840..181b571 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectMapper.java @@ -9,6 +9,8 @@ import org.opendevstack.apiservice.serviceproject.model.ProjectResponse; import org.opendevstack.apiservice.serviceproject.model.Status; +import java.text.MessageFormat; + @Mapper(componentModel = "spring") public interface ProjectMapper { @@ -16,6 +18,7 @@ public interface ProjectMapper { @Mapping(source = "status", target = "status", qualifiedByName = "mapStatus") @Mapping(source = "projectKey", target = "location", qualifiedByName = "mapLocation") + @Mapping(source = ".", target = "errorDescription", qualifiedByName = "mapErrorDescription") CreateProjectResponse toApiResponse(ProjectResponse serviceResponse); @Named("mapStatus") @@ -26,6 +29,16 @@ default String mapStatus(Status status) { return status.getDbValue(); } + @Named("mapErrorDescription") + default String mapErrorDescription(ProjectResponse serviceResponse) { + return (serviceResponse.getStatus() == Status.FAILED) + ? MessageFormat.format( + "There was an error when creating the project {0}.\n\n " + + "The error has been reported to our Support team as an incident. " + + "You will be informed about the incident via email.", serviceResponse.getProjectKey()) + : null; + } + @Named("mapLocation") default String mapLocation(String projectKey) { if (projectKey == null || projectKey.isEmpty()) { 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 d5d5c84..5e57b72 100644 --- a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerTest.java +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/ProjectControllerTest.java @@ -68,12 +68,12 @@ void createProject_whenProjectCreationException_thenReturnConflict() throws Exce assertThat(result.getStatusCode()).isEqualTo(HttpStatus.CONFLICT); assertThat(result.getBody()).isNotNull(); - assertThat(result.getBody().getError()).isEqualTo("CONFLICT"); - assertThat(result.getBody().getErrorKey()).isEqualTo("PROJECT_ALREADY_EXISTS"); - assertThat(result.getBody().getErrorDescription()).contains("Project with key 'EXISTING' already exists"); + assertThat(result.getBody().getError()).isEqualTo("Project already exists"); + assertThat(result.getBody().getErrorKey()).isEqualTo("025"); + assertThat(result.getBody().getMessage()).contains("Project with key 'EXISTING' already exists"); assertThat(result.getBody().getProjectKey()).isNull(); assertThat(result.getBody().getStatus()).isNull(); - assertThat(result.getBody().getMessage()).isNull(); + assertThat(result.getBody().getErrorDescription()).isNull(); } @Test @@ -87,12 +87,12 @@ void createProject_whenProjectKeyGenerationException_thenReturnInternalServerErr assertThat(result.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); assertThat(result.getBody()).isNotNull(); - assertThat(result.getBody().getError()).isEqualTo("INTERNAL_ERROR"); + assertThat(result.getBody().getError()).isEqualTo("Internal error"); assertThat(result.getBody().getErrorKey()).isEqualTo("PROJECT_KEY_GENERATION_FAILED"); - assertThat(result.getBody().getErrorDescription()).isEqualTo("Failed to generate a unique project key."); + 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().getMessage()).isNull(); + assertThat(result.getBody().getErrorDescription()).isNull(); } @Test @@ -121,29 +121,29 @@ void getProject_whenNotFound_thenReturnNotFound() throws Exception { assertThat(result.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); assertThat(result.getBody()).isNotNull(); - assertThat(result.getBody().getError()).isEqualTo("NOT_FOUND"); - assertThat(result.getBody().getErrorKey()).isEqualTo("PROJECT_NOT_FOUND"); - assertThat(result.getBody().getErrorDescription()).contains("UNKNOWN"); + assertThat(result.getBody().getError()).isEqualTo("Not Found"); + assertThat(result.getBody().getErrorKey()).isEqualTo("012"); + assertThat(result.getBody().getMessage()).contains("UNKNOWN"); assertThat(result.getBody().getProjectKey()).isNull(); assertThat(result.getBody().getStatus()).isNull(); - assertThat(result.getBody().getMessage()).isNull(); + assertThat(result.getBody().getErrorDescription()).isNull(); } @Test void getProject_whenServiceThrows_thenReturnInternalServerError() throws Exception { when(projectsFacade.getProject(anyString())) - .thenThrow(new ProjectCreationException("Database error")); + .thenThrow(new RuntimeException("Database error")); ResponseEntity result = sut.getProject("PROJ01"); assertThat(result.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR); assertThat(result.getBody()).isNotNull(); - assertThat(result.getBody().getError()).isEqualTo("INTERNAL_ERROR"); - assertThat(result.getBody().getErrorKey()).isEqualTo("INTERNAL_ERROR"); - assertThat(result.getBody().getErrorDescription()).isEqualTo("An error occurred while processing the request."); + assertThat(result.getBody().getError()).isEqualTo("Internal error"); + assertThat(result.getBody().getErrorKey()).isEqualTo("003"); + assertThat(result.getBody().getMessage()).isEqualTo("An error occurred while processing the request."); assertThat(result.getBody().getProjectKey()).isNull(); assertThat(result.getBody().getStatus()).isNull(); - assertThat(result.getBody().getMessage()).isNull(); + assertThat(result.getBody().getErrorDescription()).isNull(); } } diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/mapper/ProjectMapperTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/mapper/ProjectMapperTest.java index 7229bf0..b7cf95a 100644 --- a/api-project/src/test/java/org/opendevstack/apiservice/project/mapper/ProjectMapperTest.java +++ b/api-project/src/test/java/org/opendevstack/apiservice/project/mapper/ProjectMapperTest.java @@ -173,4 +173,35 @@ void to_api_response_maps_null_project_flavor() { assertEquals("PROJ03", result.getProjectKey()); assertNull(result.getProjectFlavor()); } + + @Test + void to_api_response_sets_error_description_when_status_is_failed() { + ProjectResponse serviceResponse = ProjectResponse.builder() + .projectKey("FAIL01") + .status(Status.FAILED) + .build(); + + CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse); + + assertNotNull(result); + assertEquals( + "There was an error when creating the project FAIL01.\n\n " + + "The error has been reported to our Support team as an incident. " + + "You will be informed about the incident via email.", + result.getErrorDescription() + ); + } + + @Test + void to_api_response_sets_null_error_description_when_status_is_not_failed() { + ProjectResponse serviceResponse = ProjectResponse.builder() + .projectKey("PROJ01") + .status(Status.RUNNING) + .build(); + + CreateProjectResponse result = projectMapper.toApiResponse(serviceResponse); + + assertNotNull(result); + assertNull(result.getErrorDescription()); + } } From 632c3870e07841f3e7587520f608833ccdeec974 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Wed, 18 Mar 2026 14:04:25 +0100 Subject: [PATCH 18/20] Add ErrorKey enum for standardized error handling in project responses --- .../controller/ProjectResponseFactory.java | 1 + .../project/exception/ErrorKey.java | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java index 3fe80e9..b1ed5d2 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java @@ -1,5 +1,6 @@ package org.opendevstack.apiservice.project.controller; +import org.opendevstack.apiservice.project.exception.ErrorKey; import org.opendevstack.apiservice.project.model.CreateProjectResponse; public final class ProjectResponseFactory { diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java new file mode 100644 index 0000000..d50224a --- /dev/null +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java @@ -0,0 +1,48 @@ +package org.opendevstack.apiservice.project.exception; + +public enum ErrorKey { + + OK("000", "Success"), + PRODUCT_NOT_FOUND("001", "Not Found"), + ACCESS_DENIED("002", "Forbidden"), + INTERNAL_ERROR("003", "Internal error"), + INVALID_AUTH_HEADER("004", "Bad request"), + MISSING_AUTH_HEADER("005", "Bad request"), + INVALID_PARAMETERS("006", "Bad request"), + X2_ACCOUNT_MISSING_GROUPS("007", "Not Found"), + ONLY_INVITED_PROJECT("008", "Forbidden"), + ONCE_PER_PROJECT("009", "Forbidden"), + COMPONENT_PARAM_NOT_MEET_REGEX("010", "Bad request"), + INVALID_LOCATION("011", "Bad request"), + PROJECT_NOT_FOUND("012", "Not Found"), + COMPONENT_NOT_FOUND("013", "Not Found"), + BAD_REQUEST_BODY("014", "Bad request"), + FORBIDDEN("015", "Forbidden"), + DUPLICATE_RECORD("016", "Record already exists"), + COMPONENT_PARAM_INVALID_FORMAT("017", "Bad request"), + PROJECT_KEY_INVALID_FORMAT("018", "projectKey not met the pattern ^[A-Z] {2}[A-Z0-9] {1,8}$"), + PROJECT_NAME_INVALID_FORMAT("019", "projectName not met the pattern ^[A-Za-z0-9 ] {0,80}$"), + PROJECT_DESCRIPTION_INVALID_FORMAT("020", "projectDescription not met the pattern ^.{0,255}$"), + PROJECT_OWNER_INVALID_FORMAT("021", "projectOwner not met the pattern ^[a-z]{1,10}$"), + PROJECT_X2ACCOUNT_INVALID_FORMAT("022", "projectX2Account not met the pattern ^x2[a-zA-Z0-9]{0,13}$"), + BAD_REQUEST_FLAVOR_CONFIG_ITEM("023", "Project flavour and config item cannot be both null"), + MANDATORY_OWNER("024", "Owner must be present if the X2 account is present"), + PROJECT_ALREADY_EXISTS("025", "Project already exists"), + PROJECT_SAME_PROJECT_NAME_ALREADY_EXISTS("026", "Project with same project name already exists"); + + private String key; + private String message; + + ErrorKey(String key, String message) { + this.key = key; + this.message = message; + } + + public String getKey() { + return this.key; + } + + public String getMessage() { + return this.message; + } +} From 5473269b170c3731207cec4d079880e660345104 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Wed, 18 Mar 2026 14:19:21 +0100 Subject: [PATCH 19/20] Refactor ErrorKey enum to use ErrorMessage constants for improved error handling --- .../project/exception/ErrorKey.java | 30 +++++++++---------- .../project/exception/ErrorMessage.java | 10 +++++++ 2 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorMessage.java diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java index d50224a..4ee644d 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java @@ -3,23 +3,23 @@ public enum ErrorKey { OK("000", "Success"), - PRODUCT_NOT_FOUND("001", "Not Found"), - ACCESS_DENIED("002", "Forbidden"), + PRODUCT_NOT_FOUND("001", ErrorMessage.NOT_FOUND), + ACCESS_DENIED("002", ErrorMessage.FORBIDDEN), INTERNAL_ERROR("003", "Internal error"), - INVALID_AUTH_HEADER("004", "Bad request"), - MISSING_AUTH_HEADER("005", "Bad request"), - INVALID_PARAMETERS("006", "Bad request"), - X2_ACCOUNT_MISSING_GROUPS("007", "Not Found"), - ONLY_INVITED_PROJECT("008", "Forbidden"), - ONCE_PER_PROJECT("009", "Forbidden"), - COMPONENT_PARAM_NOT_MEET_REGEX("010", "Bad request"), - INVALID_LOCATION("011", "Bad request"), - PROJECT_NOT_FOUND("012", "Not Found"), - COMPONENT_NOT_FOUND("013", "Not Found"), - BAD_REQUEST_BODY("014", "Bad request"), - FORBIDDEN("015", "Forbidden"), + INVALID_AUTH_HEADER("004", ErrorMessage.BAD_REQUEST), + MISSING_AUTH_HEADER("005", ErrorMessage.BAD_REQUEST), + INVALID_PARAMETERS("006", ErrorMessage.BAD_REQUEST), + X2_ACCOUNT_MISSING_GROUPS("007", ErrorMessage.NOT_FOUND), + ONLY_INVITED_PROJECT("008", ErrorMessage.FORBIDDEN), + ONCE_PER_PROJECT("009", ErrorMessage.FORBIDDEN), + COMPONENT_PARAM_NOT_MEET_REGEX("010", ErrorMessage.BAD_REQUEST), + INVALID_LOCATION("011", ErrorMessage.BAD_REQUEST), + PROJECT_NOT_FOUND("012", ErrorMessage.NOT_FOUND), + COMPONENT_NOT_FOUND("013", ErrorMessage.NOT_FOUND), + BAD_REQUEST_BODY("014", ErrorMessage.BAD_REQUEST), + FORBIDDEN("015", ErrorMessage.FORBIDDEN), DUPLICATE_RECORD("016", "Record already exists"), - COMPONENT_PARAM_INVALID_FORMAT("017", "Bad request"), + COMPONENT_PARAM_INVALID_FORMAT("017", ErrorMessage.BAD_REQUEST), PROJECT_KEY_INVALID_FORMAT("018", "projectKey not met the pattern ^[A-Z] {2}[A-Z0-9] {1,8}$"), PROJECT_NAME_INVALID_FORMAT("019", "projectName not met the pattern ^[A-Za-z0-9 ] {0,80}$"), PROJECT_DESCRIPTION_INVALID_FORMAT("020", "projectDescription not met the pattern ^.{0,255}$"), diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorMessage.java b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorMessage.java new file mode 100644 index 0000000..669af03 --- /dev/null +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorMessage.java @@ -0,0 +1,10 @@ +package org.opendevstack.apiservice.project.exception; + +public class ErrorMessage { + + public static final String NOT_FOUND = "Not Found"; + public static final String FORBIDDEN = "Forbidden"; + public static final String BAD_REQUEST = "Bad Request"; + + public ErrorMessage() {} +} From b3f64063b9e02087a0f16a4a03dd2192625a1c27 Mon Sep 17 00:00:00 2001 From: Angel Martinez Date: Wed, 18 Mar 2026 14:38:41 +0100 Subject: [PATCH 20/20] Prevent instantiation of ErrorMessage class by making the constructor private --- .../apiservice/project/exception/ErrorMessage.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorMessage.java b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorMessage.java index 669af03..767ab2e 100644 --- a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorMessage.java +++ b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorMessage.java @@ -5,6 +5,8 @@ public class ErrorMessage { public static final String NOT_FOUND = "Not Found"; public static final String FORBIDDEN = "Forbidden"; public static final String BAD_REQUEST = "Bad Request"; - - public ErrorMessage() {} + + private ErrorMessage() { + // prevent instantiation + } }