diff --git a/api-project/openapi/api-project.yaml b/api-project/openapi/api-project.yaml
index 30820a9..26f00f7 100644
--- a/api-project/openapi/api-project.yaml
+++ b/api-project/openapi/api-project.yaml
@@ -138,19 +138,21 @@ components:
maxLength: 255
projectFlavor:
type: string
- description: Flavor of the project. Either projectFlavor or configurationItem must be provided.
+ description: Project flavor. Must be provided if configurationItem is not present. If both projectFlavor and configurationItem are missing, error BAD_REQUEST_FLAVOR_CONFIG_ITEM (023) is returned.
configurationItem:
type: string
- description: Configuration item for the project. Either projectFlavor or configurationItem must be provided.
+ description: Configuration item. Must be present if projectFlavor is not provided. If both projectFlavor and configurationItem are missing, error BAD_REQUEST_FLAVOR_CONFIG_ITEM (023) is returned.
location:
type: string
- description: Location of the project.
+ description: Location of the project. Must be one of the allowed locations. If not in the allowed list, error INVALID_LOCATION (011) is returned.
x2OdsAccount:
type: string
description: Technical account of the project.
+ pattern: "^x2[a-zA-Z0-9]{0,13}$"
owner:
type: string
- description: Owner of the project.
+ description: Owner of the project. If x2OdsAccount is provided but owner is missing, error MANDATORY_OWNER (024) is returned.
+ pattern: "^[a-z]{1,10}$"
oneOf:
- required: [projectFlavor]
- required: [configurationItem]
diff --git a/api-project/pom.xml b/api-project/pom.xml
index e77eda9..3a7962e 100644
--- a/api-project/pom.xml
+++ b/api-project/pom.xml
@@ -33,6 +33,13 @@
org.springframework.boot
spring-boot-starter-validation
+
+
+ org.springframework.boot
+ spring-boot-devtools
+ runtime
+ true
+
org.springdoc
@@ -44,6 +51,12 @@
service-projects
${project.version}
+
+
+ org.opendevstack.apiservice
+ persistence
+ ${project.version}
+
io.jsonwebtoken
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 89aadde..b714133 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
@@ -4,11 +4,9 @@
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.opendevstack.apiservice.project.api.ProjectsApi;
-import org.opendevstack.apiservice.project.exception.ProjectCreationException;
import org.opendevstack.apiservice.project.facade.ProjectsFacade;
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
-import org.opendevstack.apiservice.project.exception.ProjectKeyGenerationException;
import org.opendevstack.apiservice.project.validation.ProjectRequestValidator;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -19,6 +17,8 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import java.util.UUID;
+
@RestController
@RequestMapping(ProjectController.API_BASE_PATH)
@AllArgsConstructor
@@ -37,44 +37,27 @@ public class ProjectController implements ProjectsApi {
@Override
public ResponseEntity createProject(@Valid @RequestBody CreateProjectRequest createProjectRequest) {
projectRequestValidator.validate(createProjectRequest);
- try {
- 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)
- .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)
- .header(HTTP_HEADER_LOCATION, API_BASE_PATH)
- .body(ProjectResponseFactory.projectKeyGenerationFailed(API_BASE_PATH));
- }
+ UUID clientId = UUID.fromString("00000000-0000-0000-0000-000000000001");
+ CreateProjectResponse projectResponse = projectsFacade.createProject(createProjectRequest, clientId);
+ projectResponse.setLocation(API_BASE_PATH + "/" + projectResponse.getProjectKey());
+ return ResponseEntity
+ .status(HttpStatus.OK)
+ .header(HTTP_HEADER_LOCATION, API_BASE_PATH)
+ .body(projectResponse);
}
@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)
- .header(HTTP_HEADER_LOCATION, location)
- .body(ProjectResponseFactory.notFound(projectKey, location));
- }
- 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)
+ CreateProjectResponse response = projectsFacade.getProject(projectKey);
+ if (response == null) {
+ return ResponseEntity.status(HttpStatus.NOT_FOUND)
.header(HTTP_HEADER_LOCATION, location)
- .body(ProjectResponseFactory.internalError(location));
+ .body(ProjectResponseFactory.notFound(projectKey, location));
}
+ return ResponseEntity.status(HttpStatus.OK)
+ .header(HTTP_HEADER_LOCATION, location)
+ .body(response);
}
}
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 b1ed5d2..caf14c9 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
@@ -39,6 +39,14 @@ public static CreateProjectResponse internalError(String location) {
location);
}
+ public static CreateProjectResponse internalError(String location, String message) {
+ return error(
+ ErrorKey.INTERNAL_ERROR.getMessage(),
+ ErrorKey.INTERNAL_ERROR.getKey(),
+ message,
+ location);
+ }
+
private static CreateProjectResponse error(String error, String errorKey, String message, String location) {
CreateProjectResponse response = new CreateProjectResponse();
response.setError(error);
diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandler.java b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandler.java
index 6aa6db2..56308e1 100644
--- a/api-project/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandler.java
+++ b/api-project/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandler.java
@@ -1,8 +1,11 @@
package org.opendevstack.apiservice.project.controller.advice;
import lombok.extern.slf4j.Slf4j;
+import org.opendevstack.apiservice.externalservice.aap.exception.AutomationPlatformException;
import org.opendevstack.apiservice.project.controller.ProjectController;
+import org.opendevstack.apiservice.project.exception.ClientAppNotRegisteredException;
import org.opendevstack.apiservice.project.exception.ErrorKey;
+import org.opendevstack.apiservice.project.exception.ProjectCreationException;
import org.opendevstack.apiservice.project.exception.ProjectValidationException;
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
import org.springframework.http.HttpStatus;
@@ -23,7 +26,9 @@ public class ProjectExceptionHandler {
"projectName", ErrorKey.PROJECT_NAME_INVALID_FORMAT,
"projectDescription", ErrorKey.PROJECT_DESCRIPTION_INVALID_FORMAT,
"projectFlavor", ErrorKey.BAD_REQUEST_FLAVOR_CONFIG_ITEM,
- "configurationItem", ErrorKey.BAD_REQUEST_FLAVOR_CONFIG_ITEM
+ "configurationItem", ErrorKey.BAD_REQUEST_FLAVOR_CONFIG_ITEM,
+ "x2OdsAccount", ErrorKey.PROJECT_X2ACCOUNT_INVALID_FORMAT,
+ "owner", ErrorKey.PROJECT_OWNER_INVALID_FORMAT
);
@ExceptionHandler(MethodArgumentNotValidException.class)
@@ -55,6 +60,18 @@ public ResponseEntity handleMethodArgumentNotValidExcepti
return ResponseEntity.badRequest().body(response);
}
+ @ExceptionHandler(ClientAppNotRegisteredException.class)
+ public ResponseEntity handleClientAppNotRegisteredException(
+ ClientAppNotRegisteredException ex) {
+ log.warn("ClientApp registration error: {}", ex.getMessage());
+ CreateProjectResponse response = new CreateProjectResponse();
+ response.setLocation(ProjectController.API_BASE_PATH);
+ response.setError(HttpStatus.FORBIDDEN.getReasonPhrase());
+ response.setErrorKey(ErrorKey.CLIENT_APP_NOT_REGISTERED.getKey());
+ response.setMessage(ErrorKey.CLIENT_APP_NOT_REGISTERED.getMessage());
+ return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
+ }
+
@ExceptionHandler(ProjectValidationException.class)
public ResponseEntity handleValidationException(ProjectValidationException ex) {
log.warn("Validation error: {}", ex.getMessage());
@@ -66,6 +83,41 @@ public ResponseEntity handleValidationException(ProjectVa
response.setMessage(errorKey.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
+
+ @ExceptionHandler(ProjectCreationException.class)
+ public ResponseEntity handleProjectCreationException(
+ ProjectCreationException ex) {
+ log.error("Project creation error: {}", ex.getMessage(), ex);
+ CreateProjectResponse response = new CreateProjectResponse();
+ response.setLocation(ProjectController.API_BASE_PATH);
+ response.setError(ErrorKey.INTERNAL_ERROR.getMessage());
+ response.setErrorKey(ErrorKey.INTERNAL_ERROR.getKey());
+ response.setMessage(ex.getMessage());
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+ }
+
+ @ExceptionHandler(AutomationPlatformException.class)
+ public ResponseEntity handleAutomationPlatformException(
+ AutomationPlatformException ex) {
+ log.error("Failed to execute automated job: {}", ex.getMessage(), ex);
+ CreateProjectResponse response = new CreateProjectResponse();
+ response.setLocation(ProjectController.API_BASE_PATH);
+ response.setError(ErrorKey.INTERNAL_ERROR.getMessage());
+ response.setErrorKey(ErrorKey.INTERNAL_ERROR.getKey());
+ response.setMessage(ex.getMessage());
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+ }
+
+ @ExceptionHandler(Exception.class)
+ public ResponseEntity handleGenericException(Exception ex) {
+ log.error("Unexpected error: {}", ex.getMessage(), ex);
+ CreateProjectResponse response = new CreateProjectResponse();
+ response.setLocation(ProjectController.API_BASE_PATH);
+ response.setError(ErrorKey.INTERNAL_ERROR.getMessage());
+ response.setErrorKey(ErrorKey.INTERNAL_ERROR.getKey());
+ response.setMessage("An error occurred while processing the request.");
+ return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
+ }
}
diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ClientAppNotRegisteredException.java b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ClientAppNotRegisteredException.java
new file mode 100644
index 0000000..a3b8c75
--- /dev/null
+++ b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ClientAppNotRegisteredException.java
@@ -0,0 +1,8 @@
+package org.opendevstack.apiservice.project.exception;
+
+public class ClientAppNotRegisteredException extends RuntimeException {
+
+ public ClientAppNotRegisteredException(String clientId) {
+ super(String.format("ClientApp with clientId '%s' is not registered", clientId));
+ }
+}
\ No newline at end of file
diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ErrorKey.java
index 4ee644d..fd7904b 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
@@ -28,7 +28,10 @@ public enum ErrorKey {
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");
+ PROJECT_SAME_PROJECT_NAME_ALREADY_EXISTS("026", "Project with same project name already exists"),
+ CLIENT_APP_NOT_REGISTERED("027", "ClientApp not registered, manual registration required"),
+ INVALID_PROJECT_FLAVOR("028", ErrorMessage.BAD_REQUEST),
+ INVALID_CONFIG_ITEM("029", ErrorMessage.BAD_REQUEST);
private String key;
private String message;
diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectCreationException.java b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectCreationException.java
index 87e12af..8189cdc 100644
--- a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectCreationException.java
+++ b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectCreationException.java
@@ -1,6 +1,6 @@
package org.opendevstack.apiservice.project.exception;
-public class ProjectCreationException extends Exception {
+public class ProjectCreationException extends RuntimeException {
public ProjectCreationException(String message) {
super(message);
diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectKeyGenerationException.java b/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectKeyGenerationException.java
deleted file mode 100644
index be9b66a..0000000
--- a/api-project/src/main/java/org/opendevstack/apiservice/project/exception/ProjectKeyGenerationException.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package org.opendevstack.apiservice.project.exception;
-
-public class ProjectKeyGenerationException extends Exception {
-
- public ProjectKeyGenerationException(String message) {
- super(message);
- }
-
- public ProjectKeyGenerationException(String message, Throwable cause) {
- super(message, cause);
- }
-}
\ 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 b30ad4f..f8b0112 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
@@ -1,14 +1,13 @@
package org.opendevstack.apiservice.project.facade;
-import org.opendevstack.apiservice.project.exception.ProjectCreationException;
-import org.opendevstack.apiservice.project.exception.ProjectKeyGenerationException;
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
+import java.util.UUID;
+
public interface ProjectsFacade {
- CreateProjectResponse createProject(CreateProjectRequest request)
- throws ProjectCreationException, ProjectKeyGenerationException;
+ CreateProjectResponse createProject(CreateProjectRequest request, UUID clientId);
CreateProjectResponse getProject(String projectKey);
}
\ No newline at end of file
diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectCreationCommand.java b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectCreationCommand.java
new file mode 100644
index 0000000..2267c0b
--- /dev/null
+++ b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectCreationCommand.java
@@ -0,0 +1,31 @@
+package org.opendevstack.apiservice.project.facade.impl;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.util.UUID;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ProjectCreationCommand {
+
+ private String projectKey;
+
+ private String projectName;
+
+ private String projectDescription;
+
+ private String projectFlavor;
+
+ private String configurationItem;
+
+ private String location;
+
+ private String x2OdsAccount;
+
+ private String owner;
+
+ private UUID clientId;
+}
\ No newline at end of file
diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectCreationCommandBuilder.java b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectCreationCommandBuilder.java
new file mode 100644
index 0000000..10b087d
--- /dev/null
+++ b/api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectCreationCommandBuilder.java
@@ -0,0 +1,142 @@
+package org.opendevstack.apiservice.project.facade.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.logging.log4j.util.Strings;
+import org.opendevstack.apiservice.persistence.entity.ClientAppEntity;
+import org.opendevstack.apiservice.persistence.entity.ClientAppProjectFlavorEntity;
+import org.opendevstack.apiservice.project.exception.ErrorKey;
+import org.opendevstack.apiservice.project.exception.ProjectCreationException;
+import org.opendevstack.apiservice.project.exception.ProjectValidationException;
+import org.opendevstack.apiservice.project.model.CreateProjectRequest;
+import org.opendevstack.apiservice.serviceproject.exception.ProjectExistenceServiceException;
+import org.opendevstack.apiservice.serviceproject.exception.ProjectKeyGenerationException;
+import org.opendevstack.apiservice.serviceproject.service.GenerateProjectKeyService;
+import org.opendevstack.apiservice.serviceproject.service.ProjectExistenceService;
+import org.springframework.stereotype.Component;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+
+@Component
+@RequiredArgsConstructor
+@Slf4j
+public class ProjectCreationCommandBuilder {
+
+ private final GenerateProjectKeyService generateProjectKeyService;
+
+ private final ProjectExistenceService projectExistenceService;
+
+ public ProjectCreationCommand build(CreateProjectRequest request, ClientAppEntity clientApp) {
+ ClientAppProjectFlavorEntity flavor = resolveFlavor(request, clientApp);
+
+ String projectFlavor = firstNonBlank(request.getProjectFlavor(), flavor.getName());
+ String configurationItem = firstNonBlank(request.getConfigurationItem(), flavor.getConfigItem());
+ String owner = firstNonBlank(request.getOwner(), flavor.getProjectOwner());
+ String x2account = firstNonBlank(request.getX2OdsAccount(), flavor.getServiceAccount());
+ String location = firstNonBlank(request.getLocation(), flavor.getLocation());
+ String projectKey = resolveProjectKey(request.getProjectKey(), flavor);
+ String projectName = firstNonBlank(request.getProjectName(), projectKey);
+ String projectDescription = firstNonBlank(request.getProjectDescription(), "project " + projectFlavor);
+
+ return new ProjectCreationCommand(
+ projectKey,
+ projectName,
+ projectDescription,
+ projectFlavor,
+ configurationItem,
+ location,
+ x2account,
+ owner,
+ clientApp.getId());
+ }
+
+ private ClientAppProjectFlavorEntity resolveFlavor(CreateProjectRequest request, ClientAppEntity clientApp) {
+ List flavors = clientApp.getProjectFlavors();
+
+ if (flavors == null || flavors.isEmpty()) {
+ log.warn("ClientApp '{}' has no project flavors configured", clientApp.getClientId());
+ throw new ProjectValidationException(ErrorKey.INVALID_PROJECT_FLAVOR);
+ }
+
+ if (Strings.isNotEmpty(request.getProjectFlavor())) {
+ return resolveByFlavorName(request.getProjectFlavor(), flavors, clientApp.getClientId());
+ }
+
+ if (Strings.isNotEmpty(request.getConfigurationItem())) {
+ return resolveByConfigurationItem(request.getConfigurationItem(), flavors, clientApp.getClientId());
+ }
+
+ throw new ProjectValidationException(ErrorKey.BAD_REQUEST_FLAVOR_CONFIG_ITEM);
+ }
+
+ private ClientAppProjectFlavorEntity resolveByFlavorName(
+ String flavorName, List flavors, UUID clientId) {
+ return flavors.stream()
+ .filter(f -> flavorName.equals(f.getName()))
+ .findFirst()
+ .orElseThrow(() -> {
+ log.warn("Flavor '{}' is not configured for clientApp '{}'", flavorName, clientId);
+ return new ProjectValidationException(ErrorKey.INVALID_PROJECT_FLAVOR);
+ });
+ }
+
+ private ClientAppProjectFlavorEntity resolveByConfigurationItem(
+ String configurationItem, List flavors, UUID clientId) {
+ List matchingFlavors = flavors.stream()
+ .filter(f -> configurationItem.equals(f.getConfigItem()))
+ .toList();
+
+ if (matchingFlavors.size() != 1) {
+ log.warn("ConfigItem '{}' does not match exactly one flavor for clientApp '{}'",
+ configurationItem, clientId);
+ throw new ProjectValidationException(ErrorKey.INVALID_CONFIG_ITEM);
+ }
+
+ ClientAppProjectFlavorEntity matchedFlavor = matchingFlavors.getFirst();
+
+ if (!isAllowedConfigItem(configurationItem, matchedFlavor)) {
+ log.warn("ConfigItem '{}' is not in the allowed list for flavor '{}' of clientApp '{}'",
+ configurationItem, matchedFlavor.getName(), clientId);
+ throw new ProjectValidationException(ErrorKey.INVALID_CONFIG_ITEM);
+ }
+
+ return matchedFlavor;
+ }
+
+ private String resolveProjectKey(String existingProjectKey, ClientAppProjectFlavorEntity flavor) {
+ try {
+ if (Strings.isNotEmpty(existingProjectKey)) {
+ if (!projectExistenceService.isProjectFound(existingProjectKey)) {
+ return existingProjectKey;
+ }
+
+ throw new ProjectValidationException(ErrorKey.PROJECT_ALREADY_EXISTS);
+ }
+
+ String pattern = flavor.getProjectKeyPattern();
+
+ return generateProjectKeyService.generateProjectKey(pattern);
+ } catch (ProjectKeyGenerationException e) {
+ throw new ProjectCreationException("Error generating the project key", e);
+ } catch (ProjectExistenceServiceException e) {
+ throw new ProjectCreationException("Error checking if the generated key exists: " + e.getMessage(), e);
+ }
+ }
+
+ private boolean isAllowedConfigItem(String configurationItem, ClientAppProjectFlavorEntity flavor) {
+ String[] allowedConfigItems = flavor.getAllowedConfigItems();
+
+ if (allowedConfigItems == null || allowedConfigItems.length == 0) {
+ return true;
+ }
+
+ return Arrays.asList(allowedConfigItems).contains(configurationItem);
+ }
+
+ private String firstNonBlank(String preferred, String fallback) {
+ return Strings.isNotEmpty(preferred) ? preferred : fallback;
+ }
+}
+
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 ea66ee8..87a9046 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
@@ -1,31 +1,92 @@
package org.opendevstack.apiservice.project.facade.impl;
+import lombok.extern.slf4j.Slf4j;
+import org.opendevstack.apiservice.externalservice.aap.model.AutomationExecutionResult;
+import org.opendevstack.apiservice.externalservice.aap.service.AutomationPlatformService;
+import org.opendevstack.apiservice.persistence.entity.ClientAppEntity;
import org.opendevstack.apiservice.project.exception.ProjectCreationException;
-import org.opendevstack.apiservice.project.exception.ProjectKeyGenerationException;
import org.opendevstack.apiservice.project.facade.ProjectsFacade;
+import org.opendevstack.apiservice.project.mapper.AutomationParametersMapper;
+import org.opendevstack.apiservice.project.mapper.ProjectCreationResponseMapper;
import org.opendevstack.apiservice.project.mapper.ProjectMapper;
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
+import org.opendevstack.apiservice.project.service.ClientAppService;
+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 org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
+import java.util.Map;
+import java.util.UUID;
+
@Component("apiProjectFacadeImpl")
+@Slf4j
public class ProjectsFacadeImpl implements ProjectsFacade {
+ @Value("${apis.projects.ansible-workflow-name}")
+ private String createProjectWorkflow;
+
private final ProjectService projectService;
+
private final ProjectMapper projectMapper;
+
+ private final AutomationParametersMapper automationParametersMapper;
+
+ private final ProjectCreationResponseMapper projectCreationResponseMapper;
+
+ private final ProjectCreationCommandBuilder projectCreationCommandBuilder;
+
+ private final ClientAppService clientAppService;
+
+ private final AutomationPlatformService automationPlatformService;
public ProjectsFacadeImpl(
ProjectService projectService,
- ProjectMapper projectMapper) {
+ ProjectMapper projectMapper,
+ AutomationParametersMapper automationParametersMapper,
+ ProjectCreationResponseMapper projectCreationResponseMapper,
+ ProjectCreationCommandBuilder projectCreationCommandBuilder,
+ ClientAppService clientAppService,
+ AutomationPlatformService automationPlatformService) {
this.projectService = projectService;
this.projectMapper = projectMapper;
+ this.automationParametersMapper = automationParametersMapper;
+ this.projectCreationResponseMapper = projectCreationResponseMapper;
+ this.projectCreationCommandBuilder = projectCreationCommandBuilder;
+ this.clientAppService = clientAppService;
+ this.automationPlatformService = automationPlatformService;
}
@Override
- public CreateProjectResponse createProject(CreateProjectRequest request)
- throws ProjectCreationException, ProjectKeyGenerationException {
- return projectMapper.toApiResponse(projectService.createProject(projectMapper.toServiceRequest(request)));
+ public CreateProjectResponse createProject(CreateProjectRequest request, UUID clientId) {
+
+ ClientAppEntity clientApp = clientAppService.findByClientId(clientId);
+
+ ProjectCreationCommand command = projectCreationCommandBuilder.build(request, clientApp);
+
+ ProjectRequest projectRequest = projectMapper.toServiceRequest(command);
+ projectRequest.setStatus(Status.PENDING);
+
+ ProjectResponse project = projectService.saveProject(projectRequest);
+
+ String projectId = project.getProjectId().toString();
+ Map workflowParameters = automationParametersMapper.toWorkflowParameters(command, projectId);
+
+ AutomationExecutionResult automationExecutionResult = automationPlatformService
+ .executeWorkflow(createProjectWorkflow, workflowParameters);
+
+ if (automationExecutionResult.isSuccessful()) {
+ return projectCreationResponseMapper.toSuccessResponse(command, project);
+ } else {
+ projectRequest.setProjectId(project.getProjectId());
+ projectRequest.setStatus(Status.FAILED);
+ projectService.saveProject(projectRequest);
+ throw new ProjectCreationException("Failed to create project: "
+ + automationExecutionResult.getErrorDetails());
+ }
}
@Override
diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/AutomationParametersMapper.java b/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/AutomationParametersMapper.java
new file mode 100644
index 0000000..fa1ea7e
--- /dev/null
+++ b/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/AutomationParametersMapper.java
@@ -0,0 +1,26 @@
+package org.opendevstack.apiservice.project.mapper;
+
+import org.mapstruct.Mapper;
+import org.opendevstack.apiservice.project.facade.impl.ProjectCreationCommand;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Mapper(componentModel = "spring")
+public interface AutomationParametersMapper {
+
+ default Map toWorkflowParameters(ProjectCreationCommand command, String projectId) {
+ Map parameters = new HashMap<>();
+ parameters.put("geographic_region", command.getLocation());
+ parameters.put("project_flavor", command.getProjectFlavor());
+ parameters.put("project_owner", command.getOwner());
+ parameters.put("project_id", projectId);
+ parameters.put("configuration_item", command.getConfigurationItem());
+ parameters.put("project_key", command.getProjectKey());
+ parameters.put("special_account", command.getX2OdsAccount());
+ parameters.put("description", command.getProjectDescription());
+ parameters.put("project_name", command.getProjectName());
+ parameters.put("client_id", command.getClientId().toString());
+ return parameters;
+ }
+}
\ No newline at end of file
diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectCreationResponseMapper.java b/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectCreationResponseMapper.java
new file mode 100644
index 0000000..0eb9a53
--- /dev/null
+++ b/api-project/src/main/java/org/opendevstack/apiservice/project/mapper/ProjectCreationResponseMapper.java
@@ -0,0 +1,25 @@
+package org.opendevstack.apiservice.project.mapper;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.opendevstack.apiservice.project.facade.impl.ProjectCreationCommand;
+import org.opendevstack.apiservice.project.model.CreateProjectResponse;
+import org.opendevstack.apiservice.serviceproject.model.ProjectResponse;
+
+@Mapper(componentModel = "spring")
+public interface ProjectCreationResponseMapper {
+
+ @Mapping(target = "message", constant = "The project creation process has been successfully initiated.")
+ @Mapping(target = "status", ignore = true)
+ @Mapping(target = "httStatus", constant = "OK")
+ @Mapping(target = "errorKey", constant = "000")
+ @Mapping(target = "projectKey", source = "project.projectKey")
+ @Mapping(target = "projectFlavor", ignore = true)
+ @Mapping(target = "location", ignore = true)
+ @Mapping(target = "error", ignore = true)
+ @Mapping(target = "errorDescription", ignore = true)
+ CreateProjectResponse toSuccessResponse(ProjectCreationCommand command, ProjectResponse project);
+}
+
+
+
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 181b571..adb2032 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
@@ -3,6 +3,7 @@
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;
+import org.opendevstack.apiservice.project.facade.impl.ProjectCreationCommand;
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
import org.opendevstack.apiservice.serviceproject.model.ProjectRequest;
@@ -16,6 +17,8 @@ public interface ProjectMapper {
ProjectRequest toServiceRequest(CreateProjectRequest apiRequest);
+ ProjectRequest toServiceRequest(ProjectCreationCommand command);
+
@Mapping(source = "status", target = "status", qualifiedByName = "mapStatus")
@Mapping(source = "projectKey", target = "location", qualifiedByName = "mapLocation")
@Mapping(source = ".", target = "errorDescription", qualifiedByName = "mapErrorDescription")
diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/service/ClientAppService.java b/api-project/src/main/java/org/opendevstack/apiservice/project/service/ClientAppService.java
new file mode 100644
index 0000000..8c15318
--- /dev/null
+++ b/api-project/src/main/java/org/opendevstack/apiservice/project/service/ClientAppService.java
@@ -0,0 +1,28 @@
+package org.opendevstack.apiservice.project.service;
+
+import java.util.UUID;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.opendevstack.apiservice.persistence.entity.ClientAppEntity;
+import org.opendevstack.apiservice.persistence.repository.ClientAppRepository;
+import org.opendevstack.apiservice.project.exception.ClientAppNotRegisteredException;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@Slf4j
+public class ClientAppService {
+
+ private final ClientAppRepository clientAppRepository;
+
+ @Transactional(readOnly = true)
+ public ClientAppEntity findByClientId(UUID clientId) {
+ log.debug("Looking up ClientApp for clientId={}", clientId);
+ return clientAppRepository.findDetailedByClientId(clientId)
+ .orElseThrow(() -> {
+ log.warn("ClientApp not found for clientId={}", clientId);
+ return new ClientAppNotRegisteredException(clientId.toString());
+ });
+ }
+}
\ No newline at end of file
diff --git a/api-project/src/main/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidator.java b/api-project/src/main/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidator.java
index a964261..effe572 100644
--- a/api-project/src/main/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidator.java
+++ b/api-project/src/main/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidator.java
@@ -3,13 +3,21 @@
import org.opendevstack.apiservice.project.exception.ErrorKey;
import org.opendevstack.apiservice.project.exception.ProjectValidationException;
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
+import java.util.List;
+
@Component
public class ProjectRequestValidator {
+ @Value("${apis.projects.locations}")
+ private List locations;
+
public void validate(CreateProjectRequest request) {
validateFlavorOrConfigItem(request);
+ validateOwnerIfX2AccountIsPresent(request);
+ validateLocation(request);
}
private void validateFlavorOrConfigItem(CreateProjectRequest request) {
@@ -25,4 +33,30 @@ private void validateFlavorOrConfigItem(CreateProjectRequest request) {
);
}
}
+
+ private void validateOwnerIfX2AccountIsPresent(CreateProjectRequest request) {
+ String x2Account = request.getX2OdsAccount();
+ String owner = request.getOwner();
+
+ boolean hasX2Account = x2Account != null && !x2Account.trim().isEmpty();
+ boolean hasOwner = owner != null && !owner.trim().isEmpty();
+
+ if(hasX2Account && !hasOwner) {
+ throw new ProjectValidationException(
+ ErrorKey.MANDATORY_OWNER
+ );
+ }
+ }
+
+ private void validateLocation(CreateProjectRequest request) {
+ String location = request.getLocation();
+
+ if (location == null || location.trim().isEmpty()) {
+ return;
+ }
+
+ if (!locations.contains(location)) {
+ throw new ProjectValidationException(ErrorKey.INVALID_LOCATION);
+ }
+ }
}
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 f40526b..fbd1123 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
@@ -5,8 +5,6 @@
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import org.opendevstack.apiservice.project.exception.ProjectCreationException;
-import org.opendevstack.apiservice.project.exception.ProjectKeyGenerationException;
import org.opendevstack.apiservice.project.facade.ProjectsFacade;
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
@@ -16,10 +14,11 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.util.UUID;
+
class ProjectControllerTest {
@Mock
@@ -43,16 +42,17 @@ void tearDown() throws Exception {
}
@Test
- void create_project_returns_ok_when_creation_succeeds() throws Exception {
+ void create_project_returns_ok_when_creation_succeeds() {
CreateProjectRequest request = new CreateProjectRequest();
request.setProjectKey("PROJ01");
CreateProjectResponse serviceResponse = new CreateProjectResponse();
serviceResponse.setProjectKey("PROJ01");
serviceResponse.setStatus("Initiated");
+ serviceResponse.setErrorKey("000");
serviceResponse.setMessage("The project creation process has been successfully initiated.");
- when(projectsFacade.createProject(any(CreateProjectRequest.class)))
+ when(projectsFacade.createProject(any(CreateProjectRequest.class), any(UUID.class)))
.thenReturn(serviceResponse);
ResponseEntity result = sut.createProject(request);
@@ -62,50 +62,10 @@ void create_project_returns_ok_when_creation_succeeds() throws Exception {
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();
- verify(projectRequestValidator).validate(request);
- }
-
- @Test
- void create_project_returns_conflict_when_project_creation_exception_is_thrown() throws Exception {
- CreateProjectRequest request = new CreateProjectRequest();
- request.setProjectKey("EXISTING");
-
- when(projectsFacade.createProject(any(CreateProjectRequest.class)))
- .thenThrow(new ProjectCreationException("Project with key 'EXISTING' already exists"));
-
- ResponseEntity result = sut.createProject(request);
-
- assertThat(result.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
- assertThat(result.getBody()).isNotNull();
- assertThat(result.getBody().getError()).isEqualTo("Project already exists");
- assertThat(result.getBody().getErrorKey()).isEqualTo("025");
- assertThat(result.getBody().getMessage()).contains("Project with key 'EXISTING' already exists");
- assertThat(result.getBody().getProjectKey()).isNull();
- assertThat(result.getBody().getStatus()).isNull();
- assertThat(result.getBody().getErrorDescription()).isNull();
- verify(projectRequestValidator).validate(request);
- }
-
- @Test
- void create_project_returns_internal_server_error_when_project_key_generation_exception_is_thrown() throws Exception {
- CreateProjectRequest request = new CreateProjectRequest();
-
- when(projectsFacade.createProject(any(CreateProjectRequest.class)))
- .thenThrow(new ProjectKeyGenerationException("Failed to generate unique project key after 10 retries"));
-
- ResponseEntity result = sut.createProject(request);
-
- assertThat(result.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
- 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().getProjectKey()).isNull();
- assertThat(result.getBody().getStatus()).isNull();
+ assertThat(result.getBody().getErrorKey()).isEqualTo("000");
assertThat(result.getBody().getErrorDescription()).isNull();
verify(projectRequestValidator).validate(request);
+ verify(projectsFacade).createProject(any(CreateProjectRequest.class), any(UUID.class));
}
@Test
@@ -140,23 +100,7 @@ void get_project_returns_not_found_when_project_does_not_exist() {
assertThat(result.getBody().getProjectKey()).isNull();
assertThat(result.getBody().getStatus()).isNull();
assertThat(result.getBody().getErrorDescription()).isNull();
- }
-
- @Test
- void get_project_returns_internal_server_error_when_service_throws_exception() {
- when(projectsFacade.getProject(anyString()))
- .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("003");
- assertThat(result.getBody().getMessage()).isEqualTo("An error occurred while processing the request.");
- assertThat(result.getBody().getProjectKey()).isNull();
- assertThat(result.getBody().getStatus()).isNull();
- assertThat(result.getBody().getErrorDescription()).isNull();
+ verify(projectsFacade).getProject("UNKNOWN");
}
}
diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandlerTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandlerTest.java
index a3d5142..45435a0 100644
--- a/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandlerTest.java
+++ b/api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandlerTest.java
@@ -7,8 +7,11 @@
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.MockitoAnnotations;
+import org.opendevstack.apiservice.externalservice.aap.exception.AutomationPlatformException;
import org.opendevstack.apiservice.project.controller.ProjectController;
+import org.opendevstack.apiservice.project.exception.ClientAppNotRegisteredException;
import org.opendevstack.apiservice.project.exception.ErrorKey;
+import org.opendevstack.apiservice.project.exception.ProjectCreationException;
import org.opendevstack.apiservice.project.exception.ProjectValidationException;
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
@@ -22,9 +25,9 @@
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.fail;
class ProjectExceptionHandlerTest {
@@ -197,6 +200,71 @@ private static Stream provideValidationCases() {
"Bad Request"
)
);
- }
-}
+ }
+
+ @Test
+ void handle_project_creation_exception_returns_conflict() {
+ ProjectCreationException exception = new ProjectCreationException("error message");
+
+ ResponseEntity result = sut.handleProjectCreationException(exception);
+
+ assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, result.getStatusCode());
+ assertNotNull(result.getBody());
+ assertEquals(ProjectController.API_BASE_PATH, result.getBody().getLocation());
+ assertEquals("Internal error", result.getBody().getError());
+ assertEquals("003", result.getBody().getErrorKey());
+ assertEquals("error message", result.getBody().getMessage());
+ assertNull(result.getBody().getProjectKey());
+ assertNull(result.getBody().getStatus());
+ assertNull(result.getBody().getErrorDescription());
+ }
+
+ @Test
+ void handle_client_app_not_registered_exception_returns_forbidden() {
+ ClientAppNotRegisteredException exception = new ClientAppNotRegisteredException("client-123");
+
+ ResponseEntity result = sut.handleClientAppNotRegisteredException(exception);
+
+ assertEquals(HttpStatus.FORBIDDEN, result.getStatusCode());
+ assertNotNull(result.getBody());
+ assertEquals(ProjectController.API_BASE_PATH, result.getBody().getLocation());
+ assertEquals(HttpStatus.FORBIDDEN.getReasonPhrase(), result.getBody().getError());
+ assertEquals("027", result.getBody().getErrorKey());
+ assertEquals("ClientApp not registered, manual registration required", result.getBody().getMessage());
+ assertNull(result.getBody().getProjectKey());
+ assertNull(result.getBody().getStatus());
+ }
+
+ @Test
+ void handle_automation_platform_exception_returns_internal_server_error() {
+ AutomationPlatformException exception = new AutomationPlatformException("AAP job failed");
+
+ ResponseEntity result = sut.handleAutomationPlatformException(exception);
+
+ assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, result.getStatusCode());
+ assertNotNull(result.getBody());
+ assertEquals(ProjectController.API_BASE_PATH, result.getBody().getLocation());
+ assertEquals("Internal error", result.getBody().getError());
+ assertEquals("003", result.getBody().getErrorKey());
+ assertEquals("AAP job failed", result.getBody().getMessage());
+ assertNull(result.getBody().getProjectKey());
+ assertNull(result.getBody().getStatus());
+ }
+ @Test
+ void handle_generic_exception_returns_internal_server_error() {
+ RuntimeException exception = new RuntimeException("Database error");
+
+ ResponseEntity result = sut.handleGenericException(exception);
+
+ assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, result.getStatusCode());
+ assertNotNull(result.getBody());
+ assertEquals(ProjectController.API_BASE_PATH, result.getBody().getLocation());
+ assertEquals("Internal error", result.getBody().getError());
+ assertEquals("003", result.getBody().getErrorKey());
+ assertEquals("An error occurred while processing the request.", result.getBody().getMessage());
+ assertNull(result.getBody().getProjectKey());
+ assertNull(result.getBody().getStatus());
+ assertNull(result.getBody().getErrorDescription());
+ }
+}
diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectCreationCommandBuilderTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectCreationCommandBuilderTest.java
new file mode 100644
index 0000000..c537efd
--- /dev/null
+++ b/api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectCreationCommandBuilderTest.java
@@ -0,0 +1,180 @@
+package org.opendevstack.apiservice.project.facade.impl;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.opendevstack.apiservice.persistence.entity.ClientAppEntity;
+import org.opendevstack.apiservice.persistence.entity.ClientAppProjectFlavorEntity;
+import org.opendevstack.apiservice.project.exception.ErrorKey;
+import org.opendevstack.apiservice.project.exception.ProjectCreationException;
+import org.opendevstack.apiservice.project.exception.ProjectValidationException;
+import org.opendevstack.apiservice.project.model.CreateProjectRequest;
+import org.opendevstack.apiservice.serviceproject.exception.ProjectExistenceServiceException;
+import org.opendevstack.apiservice.serviceproject.service.GenerateProjectKeyService;
+import org.opendevstack.apiservice.serviceproject.service.ProjectExistenceService;
+
+import java.util.List;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class ProjectCreationCommandBuilderTest {
+
+ private static final UUID CLIENT_ID = UUID.fromString("00000000-0000-0000-0000-000000000001");
+
+ @Mock
+ private GenerateProjectKeyService generateProjectKeyService;
+
+ @Mock
+ private ProjectExistenceService projectExistenceService;
+
+ private ProjectCreationCommandBuilder sut;
+
+ private AutoCloseable mocks;
+
+ @BeforeEach
+ void set_up() {
+ mocks = MockitoAnnotations.openMocks(this);
+ sut = new ProjectCreationCommandBuilder(generateProjectKeyService, projectExistenceService);
+ }
+
+ @AfterEach
+ void tear_down() throws Exception {
+ mocks.close();
+ }
+
+ @Test
+ void build_resolves_defaults_from_flavor_when_request_fields_are_missing() throws ProjectExistenceServiceException {
+ ClientAppProjectFlavorEntity flavor = build_flavor("DLSS", "CI-001", new String[] {}, "eu", "owner1");
+ ClientAppEntity clientApp = build_client_app(List.of(flavor));
+ CreateProjectRequest request = build_request("DLSS", null, "KEY01");
+ request.setOwner(null);
+ request.setLocation(null);
+
+ when(projectExistenceService.isProjectFound("KEY01")).thenReturn(false);
+
+ ProjectCreationCommand result = sut.build(request, clientApp);
+
+ assertEquals("DLSS", result.getProjectFlavor());
+ assertEquals("CI-001", result.getConfigurationItem());
+ assertEquals("owner1", result.getOwner());
+ assertEquals("eu", result.getLocation());
+ assertEquals("KEY01", result.getProjectKey());
+ }
+
+ @Test
+ void build_resolves_flavor_from_configuration_item_when_flavor_is_not_provided() throws ProjectExistenceServiceException {
+ ClientAppProjectFlavorEntity flavor = build_flavor("DLSS", "CI-001", new String[] {}, "eu", "owner1");
+ ClientAppEntity clientApp = build_client_app(List.of(flavor));
+ CreateProjectRequest request = build_request(null, "CI-001", "KEY01");
+
+ when(projectExistenceService.isProjectFound("KEY01")).thenReturn(false);
+
+ ProjectCreationCommand result = sut.build(request, clientApp);
+
+ assertEquals("DLSS", result.getProjectFlavor());
+ assertEquals("CI-001", result.getConfigurationItem());
+ }
+
+ @Test
+ void build_generates_project_key_when_request_project_key_is_null() throws ProjectExistenceServiceException, org.opendevstack.apiservice.serviceproject.exception.ProjectKeyGenerationException {
+ ClientAppProjectFlavorEntity flavor = build_flavor("DLSS", "CI-001", new String[] {}, "eu", "owner1");
+ ClientAppEntity clientApp = build_client_app(List.of(flavor));
+ CreateProjectRequest request = build_request("DLSS", null, null);
+
+ when(generateProjectKeyService.generateProjectKey("DLSS%06d")).thenReturn("DLSS000001");
+ when(projectExistenceService.isProjectFound("DLSS000001")).thenReturn(false);
+
+ ProjectCreationCommand result = sut.build(request, clientApp);
+
+ assertEquals("DLSS000001", result.getProjectKey());
+ verify(generateProjectKeyService).generateProjectKey("DLSS%06d");
+ }
+
+ @Test
+ void build_throws_validation_exception_when_project_key_already_exists() throws ProjectExistenceServiceException {
+ ClientAppProjectFlavorEntity flavor = build_flavor("DLSS", "CI-001", new String[] {}, "eu", "owner1");
+ ClientAppEntity clientApp = build_client_app(List.of(flavor));
+ CreateProjectRequest request = build_request("DLSS", null, "KEY01");
+
+ when(projectExistenceService.isProjectFound("KEY01")).thenReturn(true);
+
+ ProjectValidationException ex = assertThrows(ProjectValidationException.class,
+ () -> sut.build(request, clientApp));
+ assertEquals(ErrorKey.PROJECT_ALREADY_EXISTS, ex.getErrorKey());
+ }
+
+ @Test
+ void build_throws_validation_exception_when_flavor_and_config_item_are_missing() {
+ ClientAppProjectFlavorEntity flavor = build_flavor("DLSS", "CI-001", new String[] {}, "eu", "owner1");
+ ClientAppEntity clientApp = build_client_app(List.of(flavor));
+ CreateProjectRequest request = build_request(null, null, "KEY01");
+
+ ProjectValidationException ex = assertThrows(ProjectValidationException.class,
+ () -> sut.build(request, clientApp));
+ assertEquals(ErrorKey.BAD_REQUEST_FLAVOR_CONFIG_ITEM, ex.getErrorKey());
+ }
+
+ @Test
+ void build_throws_validation_exception_when_configuration_item_matches_multiple_flavors() {
+ ClientAppEntity clientApp = build_client_app(List.of(
+ build_flavor("DLSS", "CI-001", new String[] {}, "eu", "owner1"),
+ build_flavor("AMP", "CI-001", new String[] {}, "eu", "owner2")));
+ CreateProjectRequest request = build_request(null, "CI-001", "KEY01");
+
+ ProjectValidationException ex = assertThrows(ProjectValidationException.class,
+ () -> sut.build(request, clientApp));
+ assertEquals(ErrorKey.INVALID_CONFIG_ITEM, ex.getErrorKey());
+ }
+
+ @Test
+ void build_throws_project_key_generation_exception_when_generation_fails() throws Exception {
+ ClientAppProjectFlavorEntity flavor = build_flavor("DLSS", "CI-001", new String[] {}, "eu", "owner1");
+ ClientAppEntity clientApp = build_client_app(List.of(flavor));
+ CreateProjectRequest request = build_request("DLSS", null, null);
+
+ when(generateProjectKeyService.generateProjectKey("DLSS%06d"))
+ .thenThrow(new org.opendevstack.apiservice.serviceproject.exception.ProjectKeyGenerationException("fail"));
+
+ assertThrows(ProjectCreationException.class, () -> sut.build(request, clientApp));
+ }
+
+ private CreateProjectRequest build_request(String flavor, String configItem, String projectKey) {
+ CreateProjectRequest request = new CreateProjectRequest();
+ request.setProjectKey(projectKey);
+ request.setProjectFlavor(flavor);
+ request.setConfigurationItem(configItem);
+ request.setProjectName("Test Project");
+ request.setProjectDescription("A test project");
+ request.setOwner("testowner");
+ request.setLocation("eu");
+ request.setX2OdsAccount("x2test");
+ return request;
+ }
+
+ private ClientAppEntity build_client_app(List flavors) {
+ ClientAppEntity entity = ClientAppEntity.builder()
+ .clientId(CLIENT_ID)
+ .clientName("Test App")
+ .build();
+ entity.setProjectFlavors(flavors);
+ return entity;
+ }
+
+ private ClientAppProjectFlavorEntity build_flavor(
+ String name, String configItem, String[] allowedConfigItems, String location, String projectOwner) {
+ return ClientAppProjectFlavorEntity.builder()
+ .name(name)
+ .configItem(configItem)
+ .allowedConfigItems(allowedConfigItems)
+ .projectKeyPattern(name + "%06d")
+ .location(location)
+ .projectOwner(projectOwner)
+ .build();
+ }
+}
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 89ff398..eb15edd 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
@@ -1,95 +1,175 @@
package org.opendevstack.apiservice.project.facade.impl;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mapstruct.factory.Mappers;
import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.MockitoAnnotations;
+import org.opendevstack.apiservice.externalservice.aap.model.AutomationExecutionResult;
+import org.opendevstack.apiservice.externalservice.aap.service.AutomationPlatformService;
+import org.opendevstack.apiservice.persistence.entity.ClientAppEntity;
+import org.opendevstack.apiservice.project.exception.ProjectCreationException;
+import org.opendevstack.apiservice.project.mapper.AutomationParametersMapper;
+import org.opendevstack.apiservice.project.mapper.ProjectCreationResponseMapper;
import org.opendevstack.apiservice.project.mapper.ProjectMapper;
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
+import org.opendevstack.apiservice.project.service.ClientAppService;
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;
+import java.lang.reflect.Field;
+import java.util.Map;
+import java.util.UUID;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+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.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-@ExtendWith(MockitoExtension.class)
class ProjectsFacadeImplTest {
+ private static final UUID CLIENT_ID = UUID.fromString("00000000-0000-0000-0000-000000000001");
+
@Mock
private ProjectService projectService;
-
- private final ProjectMapper projectMapper = Mappers.getMapper(ProjectMapper.class);
+
+ @Mock
+ private ProjectMapper projectMapper;
+
+ @Mock
+ private AutomationParametersMapper automationParametersMapper;
+
+ @Mock
+ private ProjectCreationResponseMapper projectCreationResponseMapper;
+
+ @Mock
+ private ProjectCreationCommandBuilder projectCreationCommandBuilder;
+
+ @Mock
+ private ClientAppService clientAppService;
+
+ @Mock
+ private AutomationPlatformService automationPlatformService;
private ProjectsFacadeImpl sut;
+ private AutoCloseable mocks;
@BeforeEach
- void setup() {
- sut = new ProjectsFacadeImpl(projectService, projectMapper);
+ void set_up() throws Exception {
+ mocks = MockitoAnnotations.openMocks(this);
+ sut = new ProjectsFacadeImpl(
+ projectService,
+ projectMapper,
+ automationParametersMapper,
+ projectCreationResponseMapper,
+ projectCreationCommandBuilder,
+ clientAppService,
+ automationPlatformService);
+
+ Field workflowField = ProjectsFacadeImpl.class.getDeclaredField("createProjectWorkflow");
+ workflowField.setAccessible(true);
+ workflowField.set(sut, "create-project-workflow");
+ }
+
+ @AfterEach
+ void tear_down() throws Exception {
+ mocks.close();
}
@Test
- void createProject_whenServiceReturnsValue_thenMapToApiModel() throws Exception {
+ void create_project_returns_success_response_when_automation_is_successful() {
CreateProjectRequest request = new CreateProjectRequest();
- request.setProjectKey("PROJ01");
-
- ProjectResponse serviceResponse =
- new ProjectResponse();
- serviceResponse.setProjectKey("PROJ01");
- serviceResponse.setStatus(Status.PENDING);
-
- when(projectService.createProject(org.mockito.ArgumentMatchers.any(
- ProjectRequest.class)))
- .thenReturn(serviceResponse);
-
- CreateProjectResponse response = sut.createProject(request);
-
- assertThat(response).isNotNull();
- assertThat(response.getProjectKey()).isEqualTo("PROJ01");
- assertThat(response.getStatus()).isEqualTo("Pending");
- verify(projectService).createProject(org.mockito.ArgumentMatchers.any(
- ProjectRequest.class));
+ ClientAppEntity clientApp = ClientAppEntity.builder().clientId(CLIENT_ID).build();
+ ProjectCreationCommand command = new ProjectCreationCommand(
+ "DLSS01", "name", "desc", "DLSS", "CI-001", "eu", "x2test", "owner", CLIENT_ID);
+ ProjectRequest serviceRequest = new ProjectRequest();
+ ProjectResponse projectResponse = ProjectResponse.builder()
+ .projectId(UUID.fromString("11111111-1111-1111-1111-111111111111"))
+ .projectKey("DLSS01")
+ .status(Status.PENDING)
+ .build();
+ CreateProjectResponse apiResponse = new CreateProjectResponse();
+ apiResponse.setStatus("Pending");
+ apiResponse.setProjectFlavor("DLSS");
+
+ when(clientAppService.findByClientId(CLIENT_ID)).thenReturn(clientApp);
+ when(projectCreationCommandBuilder.build(request, clientApp)).thenReturn(command);
+ when(projectMapper.toServiceRequest(command)).thenReturn(serviceRequest);
+ when(projectService.saveProject(serviceRequest)).thenReturn(projectResponse);
+ when(automationParametersMapper.toWorkflowParameters(command, "11111111-1111-1111-1111-111111111111"))
+ .thenReturn(Map.of("project_key", "DLSS01"));
+ when(automationPlatformService.executeWorkflow(anyString(), anyMap()))
+ .thenReturn(AutomationExecutionResult.success("job-1", "ok"));
+ when(projectCreationResponseMapper.toSuccessResponse(command, projectResponse)).thenReturn(apiResponse);
+
+ CreateProjectResponse result = sut.createProject(request, CLIENT_ID);
+
+ assertEquals("Pending", result.getStatus());
+ assertEquals("DLSS", result.getProjectFlavor());
+ verify(projectCreationCommandBuilder).build(request, clientApp);
+ verify(projectMapper).toServiceRequest(command);
+ verify(automationParametersMapper)
+ .toWorkflowParameters(command, "11111111-1111-1111-1111-111111111111");
+ verify(projectCreationResponseMapper).toSuccessResponse(command, projectResponse);
}
@Test
- void createProject_whenServiceReturnsNull_thenReturnNull() throws Exception {
+ void create_project_throws_project_creation_exception_when_automation_is_not_successful() {
CreateProjectRequest request = new CreateProjectRequest();
- when(projectService.createProject(org.mockito.ArgumentMatchers.any(
- ProjectRequest.class)))
- .thenReturn(null);
-
- CreateProjectResponse response = sut.createProject(request);
-
- assertThat(response).isNull();
+ ClientAppEntity clientApp = ClientAppEntity.builder().clientId(CLIENT_ID).build();
+ ProjectCreationCommand command = new ProjectCreationCommand(
+ "DLSS01", "name", "desc", "DLSS", "CI-001", "eu", "x2test", "owner", CLIENT_ID);
+
+ when(clientAppService.findByClientId(CLIENT_ID)).thenReturn(clientApp);
+ when(projectCreationCommandBuilder.build(request, clientApp)).thenReturn(command);
+ when(projectMapper.toServiceRequest(command)).thenReturn(new ProjectRequest());
+ when(projectService.saveProject(any(ProjectRequest.class)))
+ .thenReturn(ProjectResponse.builder()
+ .projectId(UUID.fromString("11111111-1111-1111-1111-111111111111"))
+ .projectKey("DLSS01")
+ .status(Status.PENDING)
+ .build());
+ when(automationParametersMapper.toWorkflowParameters(command, "11111111-1111-1111-1111-111111111111"))
+ .thenReturn(Map.of());
+ when(automationPlatformService.executeWorkflow(anyString(), anyMap()))
+ .thenReturn(AutomationExecutionResult.failure("job-1", "error", "workflow failed"));
+
+ assertThrows(ProjectCreationException.class, () -> sut.createProject(request, CLIENT_ID));
}
@Test
- void getProject_whenServiceReturnsValue_thenMapToApiModel() throws Exception {
- ProjectResponse serviceResponse =
- new ProjectResponse();
- serviceResponse.setProjectKey("PROJ01");
- serviceResponse.setStatus(Status.RUNNING);
+ void get_project_returns_mapped_response_when_service_returns_project() {
+ ProjectResponse serviceResponse = ProjectResponse.builder()
+ .projectKey("PROJ01")
+ .status(Status.RUNNING)
+ .build();
+ CreateProjectResponse mappedResponse = new CreateProjectResponse();
+ mappedResponse.setProjectKey("PROJ01");
+ mappedResponse.setStatus("Running");
when(projectService.getProject("PROJ01")).thenReturn(serviceResponse);
+ when(projectMapper.toApiResponse(serviceResponse)).thenReturn(mappedResponse);
- CreateProjectResponse response = sut.getProject("PROJ01");
+ CreateProjectResponse result = sut.getProject("PROJ01");
- assertThat(response).isNotNull();
- assertThat(response.getProjectKey()).isEqualTo("PROJ01");
- assertThat(response.getStatus()).isEqualTo("Running");
+ assertEquals("PROJ01", result.getProjectKey());
+ assertEquals("Running", result.getStatus());
}
@Test
- void getProject_whenServiceReturnsNull_thenReturnNull() throws Exception {
+ void get_project_returns_null_when_service_returns_null() {
when(projectService.getProject("UNKNOWN")).thenReturn(null);
+ when(projectMapper.toApiResponse(null)).thenReturn(null);
- CreateProjectResponse response = sut.getProject("UNKNOWN");
+ CreateProjectResponse result = sut.getProject("UNKNOWN");
- assertThat(response).isNull();
+ assertNull(result);
}
}
diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/service/ClientAppServiceTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/service/ClientAppServiceTest.java
new file mode 100644
index 0000000..a0737fa
--- /dev/null
+++ b/api-project/src/test/java/org/opendevstack/apiservice/project/service/ClientAppServiceTest.java
@@ -0,0 +1,58 @@
+package org.opendevstack.apiservice.project.service;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import java.util.Optional;
+import java.util.UUID;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.opendevstack.apiservice.persistence.entity.ClientAppEntity;
+import org.opendevstack.apiservice.persistence.repository.ClientAppRepository;
+import org.opendevstack.apiservice.project.exception.ClientAppNotRegisteredException;
+
+class ClientAppServiceTest {
+
+ @Mock
+ private ClientAppRepository clientAppRepository;
+ private ClientAppService sut;
+
+ @BeforeEach
+ void setUp() {
+ MockitoAnnotations.openMocks(this);
+ sut = new ClientAppService(clientAppRepository);
+ }
+
+ @Test
+ void findByClientId_returns_entity_when_client_exists() {
+
+ UUID clientId = UUID.fromString("00000000-0000-0000-0000-000000000001");
+ ClientAppEntity entity = ClientAppEntity.builder()
+ .clientId(clientId)
+ .clientName("Test App")
+ .build();
+ when(clientAppRepository.findDetailedByClientId(clientId)).thenReturn(Optional.of(entity));
+
+ ClientAppEntity result = sut.findByClientId(clientId);
+
+ assertThat(result).isNotNull();
+ assertThat(result.getClientId()).isEqualTo(clientId);
+ assertThat(result.getClientName()).isEqualTo("Test App");
+ verify(clientAppRepository).findDetailedByClientId(clientId);
+ }
+
+ @Test
+ void findByClientId_throws_exception_when_client_not_found() {
+
+ UUID clientId = UUID.fromString("00000000-0000-0000-0000-000000000001");
+ when(clientAppRepository.findDetailedByClientId(clientId)).thenReturn(Optional.empty());
+
+ assertThrows(
+ ClientAppNotRegisteredException.class,
+ () -> sut.findByClientId(clientId));
+ verify(clientAppRepository).findDetailedByClientId(clientId);
+ }
+}
\ No newline at end of file
diff --git a/api-project/src/test/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidatorTest.java b/api-project/src/test/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidatorTest.java
index 3f46360..596f39c 100644
--- a/api-project/src/test/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidatorTest.java
+++ b/api-project/src/test/java/org/opendevstack/apiservice/project/validation/ProjectRequestValidatorTest.java
@@ -8,6 +8,9 @@
import org.opendevstack.apiservice.project.exception.ProjectValidationException;
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
+import java.lang.reflect.Field;
+import java.util.List;
+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -17,8 +20,13 @@ class ProjectRequestValidatorTest {
private ProjectRequestValidator sut;
@BeforeEach
- void setUp() {
+ void setUp() throws NoSuchFieldException, IllegalAccessException {
sut = new ProjectRequestValidator();
+
+ Field locationsField = ProjectRequestValidator.class.getDeclaredField("locations");
+ locationsField.setAccessible(true);
+ locationsField.set(sut, List.of("MADRID", "BARCELONA", "SANT_CUGAT"));
+
}
@Test
@@ -29,8 +37,8 @@ void validate_throws_exception_when_project_flavor_and_config_item_both_null() {
request.setConfigurationItem(null);
ProjectValidationException exception = assertThrows(
- ProjectValidationException.class,
- () -> sut.validate(request)
+ ProjectValidationException.class,
+ () -> sut.validate(request)
);
assertEquals(ErrorKey.BAD_REQUEST_FLAVOR_CONFIG_ITEM, exception.getErrorKey());
@@ -44,8 +52,8 @@ void validate_throws_exception_when_project_flavor_and_config_item_both_empty()
request.setConfigurationItem(" ");
ProjectValidationException exception = assertThrows(
- ProjectValidationException.class,
- () -> sut.validate(request)
+ ProjectValidationException.class,
+ () -> sut.validate(request)
);
assertEquals(ErrorKey.BAD_REQUEST_FLAVOR_CONFIG_ITEM, exception.getErrorKey());
@@ -53,9 +61,9 @@ void validate_throws_exception_when_project_flavor_and_config_item_both_empty()
@ParameterizedTest
@CsvSource({
- "STANDARD, null",
- "null, JIRA",
- "STANDARD, JIRA"
+ "STANDARD, null",
+ "null, JIRA",
+ "STANDARD, JIRA"
})
void validate_succeeds_when_flavor_or_config_item_provided(String projectFlavor, String configurationItem) {
CreateProjectRequest request = new CreateProjectRequest();
@@ -65,4 +73,81 @@ void validate_succeeds_when_flavor_or_config_item_provided(String projectFlavor,
assertDoesNotThrow(() -> sut.validate(request));
}
+
+ @Test
+ void validate_throws_exception_when_x2account_present_but_owner_missing() {
+ CreateProjectRequest request = new CreateProjectRequest();
+ request.setProjectName("Valid");
+ request.setProjectFlavor("STANDARD");
+ request.setX2OdsAccount("x2abc123");
+ request.setOwner(null);
+
+ ProjectValidationException exception =
+ assertThrows(ProjectValidationException.class, () -> sut.validate(request));
+
+ assertEquals(ErrorKey.MANDATORY_OWNER, exception.getErrorKey());
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "x2abc, owner1",
+ "x2x, xowner",
+ "null, owner1",
+ "x2valid, ownerX"
+ })
+ void validate_succeeds_when_owner_present_or_x2account_missing(String x2, String owner) {
+ CreateProjectRequest request = new CreateProjectRequest();
+ request.setProjectName("Valid");
+ request.setProjectFlavor("STANDARD");
+
+ request.setX2OdsAccount("null".equals(x2) ? null : x2);
+ request.setOwner(owner);
+
+ assertDoesNotThrow(() -> sut.validate(request));
+ }
+
+ @Test
+ void validate_succeeds_when_location_is_null_or_empty() {
+ CreateProjectRequest request = new CreateProjectRequest();
+ request.setProjectName("Valid");
+ request.setProjectFlavor("STANDARD");
+
+ request.setLocation(null);
+ assertDoesNotThrow(() -> sut.validate(request));
+
+ request.setLocation("");
+ assertDoesNotThrow(() -> sut.validate(request));
+
+ request.setLocation(" ");
+ assertDoesNotThrow(() -> sut.validate(request));
+ }
+
+ @Test
+ void validate_throws_exception_when_location_not_in_allowed_list() {
+ CreateProjectRequest request = new CreateProjectRequest();
+ request.setProjectName("Valid");
+ request.setProjectFlavor("STANDARD");
+
+ request.setLocation("INVALID_LOCATION_NOT_ALLOWED");
+
+ ProjectValidationException exception =
+ assertThrows(ProjectValidationException.class, () -> sut.validate(request));
+
+ assertEquals(ErrorKey.INVALID_LOCATION, exception.getErrorKey());
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "MADRID",
+ "BARCELONA",
+ "SANT_CUGAT"
+ })
+ void validate_succeeds_when_location_is_allowed(String location) {
+ CreateProjectRequest request = new CreateProjectRequest();
+ request.setProjectName("Valid");
+ request.setProjectFlavor("STANDARD");
+ request.setLocation(location);
+
+ assertDoesNotThrow(() -> sut.validate(request));
+ }
}
\ No newline at end of file
diff --git a/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/exception/AutomationPlatformException.java b/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/exception/AutomationPlatformException.java
index 2c7686f..6a0805b 100644
--- a/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/exception/AutomationPlatformException.java
+++ b/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/exception/AutomationPlatformException.java
@@ -3,7 +3,7 @@
/**
* Exception thrown when there are issues with automation platform operations.
*/
-public class AutomationPlatformException extends Exception {
+public class AutomationPlatformException extends RuntimeException {
private final String errorCode;
diff --git a/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/service/AutomationPlatformService.java b/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/service/AutomationPlatformService.java
index 57cecb7..57b042f 100644
--- a/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/service/AutomationPlatformService.java
+++ b/external-service-aap/src/main/java/org/opendevstack/apiservice/externalservice/aap/service/AutomationPlatformService.java
@@ -22,7 +22,7 @@ public interface AutomationPlatformService extends ExternalService {
* @return the execution result
* @throws AutomationPlatformException if workflow execution fails
*/
- AutomationExecutionResult executeWorkflow(String workflowName, Map parameters) throws AutomationPlatformException;
+ AutomationExecutionResult executeWorkflow(String workflowName, Map parameters);
/**
* Executes a workflow on the automation platform asynchronously.
@@ -50,7 +50,7 @@ public interface AutomationPlatformService extends ExternalService {
* @return the workflow job status and result
* @throws AutomationPlatformException if status check fails
*/
- AutomationJobStatus getWorkflowJobStatus(String workflowId) throws AutomationPlatformException;
+ AutomationJobStatus getWorkflowJobStatus(String workflowId);
/**
diff --git a/persistence/src/main/java/org/opendevstack/apiservice/persistence/dao/ClientDaoImpl.java b/persistence/src/main/java/org/opendevstack/apiservice/persistence/dao/ClientDaoImpl.java
index 31db228..d539c21 100644
--- a/persistence/src/main/java/org/opendevstack/apiservice/persistence/dao/ClientDaoImpl.java
+++ b/persistence/src/main/java/org/opendevstack/apiservice/persistence/dao/ClientDaoImpl.java
@@ -6,6 +6,7 @@
import org.springframework.stereotype.Service;
import java.util.Optional;
+import java.util.UUID;
@Service
public class ClientDaoImpl implements ClientDao {
@@ -18,10 +19,21 @@ public ClientDaoImpl(ClientAppRepository repository) {
@Override
public Optional findByClientId(String clientId) {
- return repository.findByClientId(clientId)
+ if (clientId == null || clientId.isBlank()) {
+ return Optional.empty();
+ }
+
+ final UUID clientUuid;
+ try {
+ clientUuid = UUID.fromString(clientId);
+ } catch (IllegalArgumentException ex) {
+ return Optional.empty();
+ }
+
+ return repository.findByClientId(clientUuid)
.map(entity -> new ClientInfo(
entity.getId(),
- entity.getClientId(),
+ entity.getClientId().toString(),
entity.getClientName(),
entity.isEnabled()
));
diff --git a/persistence/src/main/java/org/opendevstack/apiservice/persistence/entity/ClientAppEntity.java b/persistence/src/main/java/org/opendevstack/apiservice/persistence/entity/ClientAppEntity.java
index 74f0875..98a1421 100644
--- a/persistence/src/main/java/org/opendevstack/apiservice/persistence/entity/ClientAppEntity.java
+++ b/persistence/src/main/java/org/opendevstack/apiservice/persistence/entity/ClientAppEntity.java
@@ -1,11 +1,16 @@
package org.opendevstack.apiservice.persistence.entity;
+import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
+import jakarta.persistence.OneToMany;
import jakarta.persistence.Table;
+
+import java.util.ArrayList;
+import java.util.List;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Builder;
@@ -31,8 +36,8 @@ public class ClientAppEntity extends AuditableEntity {
private UUID id;
/** Azure AD application/client identifier. */
- @Column(name = "client_id", nullable = false, unique = true, length = 36)
- private String clientId;
+ @Column(name = "client_id", nullable = false, unique = true)
+ private UUID clientId;
/** Optional display name for the client application. */
@Column(name = "client_name", length = 255)
@@ -43,4 +48,9 @@ public class ClientAppEntity extends AuditableEntity {
@Builder.Default
private boolean enabled = true;
+ /** Project flavor configurations associated with this client. */
+ @OneToMany(mappedBy = "clientApp", cascade = CascadeType.ALL, orphanRemoval = true)
+ @Builder.Default
+ private List projectFlavors = new ArrayList<>();
+
}
\ No newline at end of file
diff --git a/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/ClientAppRepository.java b/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/ClientAppRepository.java
index 1af9a8d..b088a85 100644
--- a/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/ClientAppRepository.java
+++ b/persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/ClientAppRepository.java
@@ -19,7 +19,7 @@ public interface ClientAppRepository extends JpaRepository findByClientId(String clientId);
+ Optional findByClientId(UUID clientId);
/**
* Returns all enabled client applications.
@@ -32,7 +32,7 @@ public interface ClientAppRepository extends JpaRepository findDetailedByClientId(String clientId);
+ Optional findDetailedByClientId(UUID clientId);
}
\ No newline at end of file
diff --git a/persistence/src/main/resources/db/changelog/migrations/004_alter_client_apps_client_id_to_uuid.sql b/persistence/src/main/resources/db/changelog/migrations/004_alter_client_apps_client_id_to_uuid.sql
new file mode 100644
index 0000000..852026f
--- /dev/null
+++ b/persistence/src/main/resources/db/changelog/migrations/004_alter_client_apps_client_id_to_uuid.sql
@@ -0,0 +1,12 @@
+--liquibase formatted sql
+
+--changeset ods:004-alter-client-apps-client-id-to-uuid
+-- Description: Converts client_apps.client_id from VARCHAR(36) to UUID to align DB and Java types.
+ALTER TABLE client_apps
+ ALTER COLUMN client_id TYPE UUID
+ USING client_id::uuid;
+
+--rollback ALTER TABLE client_apzzps
+--rollback ALTER COLUMN client_id TYPE VARCHAR(36)
+--rollback USING client_id::text;
+
diff --git a/persistence/src/test/java/org/opendevstack/apiservice/persistence/repository/ClientAppRepositoryTest.java b/persistence/src/test/java/org/opendevstack/apiservice/persistence/repository/ClientAppRepositoryTest.java
index bf31346..4d433e7 100644
--- a/persistence/src/test/java/org/opendevstack/apiservice/persistence/repository/ClientAppRepositoryTest.java
+++ b/persistence/src/test/java/org/opendevstack/apiservice/persistence/repository/ClientAppRepositoryTest.java
@@ -1,10 +1,5 @@
package org.opendevstack.apiservice.persistence.repository;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-import java.util.List;
-import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
@@ -14,13 +9,18 @@
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
-import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
/**
* Integration tests for {@link ClientAppRepository}.
*/
@@ -55,11 +55,11 @@ static void postgresProperties(DynamicPropertyRegistry registry) {
void setUp() {
repository.deleteAll();
- enabledClientApp = createClientApp("11111111-1111-1111-1111-111111111111", true);
- createClientApp("22222222-2222-2222-2222-222222222222", false);
+ enabledClientApp = createClientApp(UUID.fromString("11111111-1111-1111-1111-111111111111"), true);
+ createClientApp(UUID.fromString("22222222-2222-2222-2222-222222222222"), false);
}
- private ClientAppEntity createClientApp(String clientId, boolean enabled) {
+ private ClientAppEntity createClientApp(UUID clientId, boolean enabled) {
ClientAppEntity clientApp = ClientAppEntity.builder()
.clientId(clientId)
.clientName(enabled ? "Enabled app" : "Disabled app")
@@ -76,7 +76,7 @@ class FindByClientId {
@Test
@DisplayName("returns the matching client app")
void returnsMatchingClientApp() {
- Optional found = repository.findByClientId("11111111-1111-1111-1111-111111111111");
+ Optional found = repository.findByClientId(UUID.fromString("11111111-1111-1111-1111-111111111111"));
assertThat(found).isPresent();
assertThat(found.get().getClientName()).isEqualTo("Enabled app");
@@ -86,7 +86,7 @@ void returnsMatchingClientApp() {
@Test
@DisplayName("returns empty for unknown client id")
void returnsEmptyForUnknownClientId() {
- assertThat(repository.findByClientId("99999999-9999-9999-9999-999999999999")).isEmpty();
+ assertThat(repository.findByClientId(UUID.fromString("99999999-9999-9999-9999-999999999999"))).isEmpty();
}
}
@@ -101,7 +101,7 @@ void returnsOnlyEnabledClientApps() {
List enabledApps = repository.findByEnabledTrue();
assertThat(enabledApps).hasSize(1);
- assertThat(enabledApps.get(0).getClientId()).isEqualTo("11111111-1111-1111-1111-111111111111");
+ assertThat(enabledApps.get(0).getClientId().toString()).isEqualTo("11111111-1111-1111-1111-111111111111");
}
}
@@ -112,13 +112,13 @@ class ExistsByClientId {
@Test
@DisplayName("returns true for existing client id")
void returnsTrueForExistingClientId() {
- assertThat(repository.existsByClientId("11111111-1111-1111-1111-111111111111")).isTrue();
+ assertThat(repository.existsByClientId(UUID.fromString("11111111-1111-1111-1111-111111111111"))).isTrue();
}
@Test
@DisplayName("returns false for unknown client id")
void returnsFalseForUnknownClientId() {
- assertThat(repository.existsByClientId("99999999-9999-9999-9999-999999999999")).isFalse();
+ assertThat(repository.existsByClientId(UUID.fromString("99999999-9999-9999-9999-999999999999"))).isFalse();
}
}
diff --git a/service-projects/pom.xml b/service-projects/pom.xml
index 2a82727..f391ffe 100644
--- a/service-projects/pom.xml
+++ b/service-projects/pom.xml
@@ -69,7 +69,13 @@
org.opendevstack.apiservice
external-service-ocp
${project.version}
-
+
+
+
+ org.opendevstack.apiservice
+ external-service-aap
+ ${project.version}
+
org.projectlombok
diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/exception/ProjectExistenceServiceException.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/exception/ProjectExistenceServiceException.java
new file mode 100644
index 0000000..fa3a5d0
--- /dev/null
+++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/exception/ProjectExistenceServiceException.java
@@ -0,0 +1,12 @@
+package org.opendevstack.apiservice.serviceproject.exception;
+
+public class ProjectExistenceServiceException extends Exception {
+
+ public ProjectExistenceServiceException(String message) {
+ super(message);
+ }
+
+ public ProjectExistenceServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
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 18a6491..72060ad 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
@@ -12,6 +12,7 @@
public interface ProjectResponseMapper {
@Mapping(source = "status", target = "status", qualifiedByName = "mapStatus")
+ @Mapping(source = "id", target = "projectId")
ProjectResponse toCreateProjectResponse(ProjectEntity entity);
@Named("mapStatus")
diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectRequest.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectRequest.java
index 186ffaa..9bc0f91 100644
--- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectRequest.java
+++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/model/ProjectRequest.java
@@ -4,16 +4,32 @@
import lombok.Data;
import lombok.NoArgsConstructor;
+import java.util.UUID;
+
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProjectRequest {
+ private UUID projectId;
+
private String projectKey;
-
- private String projectKeyPattern;
-
+
private String projectName;
-
+
private String projectDescription;
+
+ private String projectFlavor;
+
+ private String configurationItem;
+
+ private String location;
+
+ private String x2OdsAccount;
+
+ private String owner;
+
+ private UUID clientId;
+
+ private Status status;
}
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 40ee62a..6db5063 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
@@ -5,15 +5,25 @@
import lombok.Data;
import lombok.NoArgsConstructor;
+import java.util.UUID;
+
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ProjectResponse {
+ private UUID projectId;
+
private String projectKey;
private Status status;
private String projectFlavor;
+
+ private String message;
+
+ private String error;
+
+ private String errorKey;
}
diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/GenerateProjectKeyService.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/GenerateProjectKeyService.java
index 289b977..b00d7bf 100644
--- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/GenerateProjectKeyService.java
+++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/GenerateProjectKeyService.java
@@ -1,11 +1,12 @@
package org.opendevstack.apiservice.serviceproject.service;
+import org.opendevstack.apiservice.serviceproject.exception.ProjectExistenceServiceException;
import org.opendevstack.apiservice.serviceproject.exception.ProjectKeyGenerationException;
public interface GenerateProjectKeyService {
String DEFAULT_PROJECT_KEY_PATTERN = "SS%06d";
- String generateProjectKey(String projectKeyPattern) throws ProjectKeyGenerationException;
+ String generateProjectKey(String projectKeyPattern) throws ProjectKeyGenerationException, ProjectExistenceServiceException;
}
diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectExistenceService.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectExistenceService.java
new file mode 100644
index 0000000..ed13614
--- /dev/null
+++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectExistenceService.java
@@ -0,0 +1,8 @@
+package org.opendevstack.apiservice.serviceproject.service;
+
+import org.opendevstack.apiservice.serviceproject.exception.ProjectExistenceServiceException;
+
+public interface ProjectExistenceService {
+
+ boolean isProjectFound(String projectKey) throws ProjectExistenceServiceException;
+}
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 c2abcc6..f2ba3aa 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
@@ -5,7 +5,7 @@
public interface ProjectService {
- ProjectResponse createProject(ProjectRequest request);
+ ProjectResponse saveProject(ProjectRequest request);
ProjectResponse getProject(String projectKey);
}
diff --git a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImpl.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImpl.java
index 2e53781..7b80fd3 100644
--- a/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImpl.java
+++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImpl.java
@@ -1,21 +1,15 @@
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.ProjectExistenceServiceException;
import org.opendevstack.apiservice.serviceproject.exception.ProjectKeyGenerationException;
import org.opendevstack.apiservice.serviceproject.service.GenerateProjectKeyService;
+import org.opendevstack.apiservice.serviceproject.service.ProjectExistenceService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.security.SecureRandom;
-import java.util.Comparator;
import java.util.Random;
-import java.util.Set;
@Service
@Slf4j
@@ -23,37 +17,29 @@ 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 ProjectExistenceService projectExistenceService;
private final Random random;
@Autowired
- public GenerateProjectKeyServiceImpl(BitbucketService bitbucketService, JiraService jiraService,
- OpenshiftService openshiftService) {
- this(bitbucketService, jiraService, openshiftService, new SecureRandom());
+ public GenerateProjectKeyServiceImpl(ProjectExistenceService projectExistenceService) {
+ this(projectExistenceService, new SecureRandom());
}
- GenerateProjectKeyServiceImpl(BitbucketService bitbucketService, JiraService jiraService,
- OpenshiftService openshiftService, Random random) {
- this.bitbucketService = bitbucketService;
- this.jiraService = jiraService;
- this.openshiftService = openshiftService;
+ GenerateProjectKeyServiceImpl(ProjectExistenceService projectExistenceService, Random random) {
+ this.projectExistenceService = projectExistenceService;
this.random = random;
}
@Override
- public String generateProjectKey(String projectKeyPattern) throws ProjectKeyGenerationException {
+ public String generateProjectKey(String projectKeyPattern) throws ProjectKeyGenerationException, ProjectExistenceServiceException {
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)) {
+ if (!projectExistenceService.isProjectFound(projectKey)) {
log.debug("Generated unique project key '{}' on attempt {}", projectKey, attempt);
return projectKey;
}
@@ -71,71 +57,4 @@ private String resolveProjectKeyPattern(String projectKeyPattern) {
}
return projectKeyPattern;
}
-
- private boolean isProjectFound(String projectKey) throws ProjectKeyGenerationException {
- try {
- if (existsInAnyBitbucketInstance(projectKey)) {
- return true;
- }
-
- if (existsInAnyJiraInstance(projectKey)) {
- return true;
- }
-
- return existsInAnyOpenshift(projectKey);
- } 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/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectExistenceServiceImpl.java b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectExistenceServiceImpl.java
new file mode 100644
index 0000000..a448376
--- /dev/null
+++ b/service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectExistenceServiceImpl.java
@@ -0,0 +1,84 @@
+package org.opendevstack.apiservice.serviceproject.service.impl;
+
+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.ProjectExistenceServiceException;
+import org.opendevstack.apiservice.serviceproject.service.ProjectExistenceService;
+import org.opendevstack.apiservice.serviceproject.service.ProjectService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.Comparator;
+import java.util.Set;
+
+@Service
+public class ProjectExistenceServiceImpl implements ProjectExistenceService {
+
+ private final BitbucketService bitbucketService;
+ private final JiraService jiraService;
+ private final OpenshiftService openshiftService;
+ private final ProjectService projectService;
+
+ @Autowired
+ public ProjectExistenceServiceImpl(BitbucketService bitbucketService, JiraService jiraService,
+ OpenshiftService openshiftService, ProjectService projectService) {
+ this.bitbucketService = bitbucketService;
+ this.jiraService = jiraService;
+ this.openshiftService = openshiftService;
+ this.projectService = projectService;
+ }
+
+ @Override
+ public boolean isProjectFound(String projectKey) throws ProjectExistenceServiceException {
+ try {
+ if (existsInCollection(projectKey)) return true;
+ if (existsInAnyBitbucketInstance(projectKey)) return true;
+ if (existsInAnyJiraInstance(projectKey)) return true;
+ return existsInAnyOpenshift(projectKey);
+ } catch (BitbucketException e) {
+ throw new ProjectExistenceServiceException("Failed to check project in Bitbucket", e);
+ } catch (JiraException e) {
+ throw new ProjectExistenceServiceException("Failed to check project in Jira", e);
+ } catch (OpenshiftException e) {
+ throw new ProjectExistenceServiceException("Failed to check project in Openshift", e);
+ } catch (RuntimeException e) {
+ throw new ProjectExistenceServiceException("Unexpected error while checking project existence", e);
+ }
+ }
+
+ private boolean existsInCollection(String projectKey) {
+ return projectService.getProject(projectKey) != null;
+ }
+
+ private boolean existsInAnyBitbucketInstance(String projectKey) throws BitbucketException {
+ Set instances = bitbucketService.getAvailableInstances();
+ for (String instance : instances.stream().sorted(Comparator.naturalOrder()).toList()) {
+ if (bitbucketService.projectExists(instance, 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 instance : instances.stream().sorted(Comparator.naturalOrder()).toList()) {
+ if (jiraService.projectExists(instance, 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 instance : instances.stream().sorted(Comparator.naturalOrder()).toList()) {
+ if (openshiftService.projectExists(instance, projectKey)) return true;
+ }
+ return false;
+ }
+}
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 1d8d7b6..ccd3be4 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
@@ -1,41 +1,53 @@
package org.opendevstack.apiservice.serviceproject.service.impl;
-import lombok.AllArgsConstructor;
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.persistence.entity.ProjectEntity;
import org.opendevstack.apiservice.persistence.repository.ProjectRepository;
import org.opendevstack.apiservice.serviceproject.mapper.ProjectResponseMapper;
import org.opendevstack.apiservice.serviceproject.model.ProjectRequest;
import org.opendevstack.apiservice.serviceproject.model.ProjectResponse;
-import org.opendevstack.apiservice.serviceproject.service.GenerateProjectKeyService;
import org.opendevstack.apiservice.serviceproject.service.ProjectService;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
@Slf4j
-@AllArgsConstructor
public class ProjectServiceImpl implements ProjectService {
- private final OpenshiftService openshiftService;
-
- private final BitbucketService bitbucketService;
-
- private final JiraService jiraService;
-
- private final GenerateProjectKeyService generateProjectKeyService;
+ private static final String MANAGER_ROLE = "MANAGER";
+ private static final String TEAM_ROLE = "TEAM";
+ private static final String STAKEHOLDER_ROLE = "STAKEHOLDER";
+ @Value("${ldap.group.pattern}")
+ private String ldapGroupPattern;
+
private final ProjectRepository projectRepository;
private final ProjectResponseMapper projectResponseMapper;
+
+ public ProjectServiceImpl(ProjectRepository projectRepository,
+ ProjectResponseMapper projectResponseMapper) {
+ this.projectRepository = projectRepository;
+ this.projectResponseMapper = projectResponseMapper;
+ }
@Override
- public ProjectResponse createProject(ProjectRequest request) {
- return ProjectResponse.builder().build();
+ public ProjectResponse saveProject(ProjectRequest request) {
+ ProjectEntity entity = new ProjectEntity();
+ entity.setProjectKey(request.getProjectKey());
+ entity.setProjectName(request.getProjectName());
+ entity.setProjectFlavor(request.getProjectFlavor());
+ entity.setConfigurationItem(request.getConfigurationItem());
+ entity.setDescription(request.getProjectFlavor() + " project");
+ entity.setLdapGroupManager(getLdapGroup(MANAGER_ROLE, request.getProjectKey()));
+ entity.setLdapGroupTeam(getLdapGroup(TEAM_ROLE, request.getProjectKey()));
+ entity.setLdapGroupStakeholder(getLdapGroup(STAKEHOLDER_ROLE, request.getProjectKey()));
+ entity.setStatus(request.getStatus().getDbValue());
+ entity.setLocation(request.getLocation());
+ ProjectEntity save = projectRepository.save(entity);
+ return projectResponseMapper.toCreateProjectResponse(save);
}
@Override
@@ -48,5 +60,11 @@ public ProjectResponse getProject(String projectKey) {
return null;
}
+
+ private String getLdapGroup(String role, String projectKey) {
+ return ldapGroupPattern
+ .replace("{{projectKey}}", projectKey)
+ .replace("{{role}}", role);
+ }
}
diff --git a/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImplTest.java b/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImplTest.java
index ebba523..6e5b02f 100644
--- a/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImplTest.java
+++ b/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/GenerateProjectKeyServiceImplTest.java
@@ -7,13 +7,10 @@
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 org.opendevstack.apiservice.serviceproject.service.ProjectExistenceService;
import java.util.Random;
-import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -25,13 +22,7 @@
class GenerateProjectKeyServiceImplTest {
@Mock
- private BitbucketService bitbucketService;
-
- @Mock
- private JiraService jiraService;
-
- @Mock
- private OpenshiftService openshiftService;
+ private ProjectExistenceService projectExistenceService;
@Mock
private Random random;
@@ -40,17 +31,13 @@ class GenerateProjectKeyServiceImplTest {
@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());
+ tested = new GenerateProjectKeyServiceImpl(projectExistenceService, random);
}
@Test
- void generateProjectKey_whenFirstCandidateIsFree_thenReturnKey() throws Exception {
+ void generate_project_key_returns_key_when_first_candidate_is_free() throws Exception {
when(random.nextInt(1_000_000)).thenReturn(7);
- when(bitbucketService.projectExists(anyString(), anyString())).thenReturn(false);
- when(jiraService.projectExists(anyString(), anyString())).thenReturn(false);
+ when(projectExistenceService.isProjectFound("SS000007")).thenReturn(false);
String result = tested.generateProjectKey(null);
@@ -58,14 +45,10 @@ void generateProjectKey_whenFirstCandidateIsFree_thenReturnKey() throws Exceptio
}
@Test
- void generateProjectKey_whenFirstCandidateExists_thenRetryUntilUnique() throws Exception {
+ void generate_project_key_retries_until_unique_key_found() throws Exception {
when(random.nextInt(1_000_000)).thenReturn(1, 2);
-
- when(bitbucketService.projectExists("dev", "SS000001")).thenReturn(true);
- when(jiraService.projectExists("default", "SS000001")).thenReturn(false);
-
- when(bitbucketService.projectExists("dev", "SS000002")).thenReturn(false);
- when(jiraService.projectExists("default", "SS000002")).thenReturn(false);
+ when(projectExistenceService.isProjectFound("SS000001")).thenReturn(true);
+ when(projectExistenceService.isProjectFound("SS000002")).thenReturn(false);
String result = tested.generateProjectKey("SS%06d");
@@ -73,12 +56,9 @@ void generateProjectKey_whenFirstCandidateExists_thenRetryUntilUnique() throws E
}
@Test
- void generateProjectKey_whenNoUniqueKeyAfterMaxRetries_thenThrowException() throws Exception {
+ void generate_project_key_throws_exception_when_no_unique_key_after_max_retries() throws Exception {
when(random.nextInt(1_000_000)).thenReturn(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);
-
- when(bitbucketService.projectExists(anyString(), anyString())).thenReturn(false);
-
- when(jiraService.projectExists(anyString(), anyString())).thenReturn(true);
+ when(projectExistenceService.isProjectFound(anyString())).thenReturn(true);
assertThatThrownBy(() -> tested.generateProjectKey("SS%06d"))
.isInstanceOf(ProjectKeyGenerationException.class)
@@ -86,10 +66,9 @@ void generateProjectKey_whenNoUniqueKeyAfterMaxRetries_thenThrowException() thro
}
@Test
- void generateProjectKey_whenCustomPatternProvided_thenUseIt() throws Exception {
+ void generate_project_key_uses_custom_pattern_when_provided() throws Exception {
when(random.nextInt(1_000_000)).thenReturn(42);
- when(bitbucketService.projectExists(anyString(), anyString())).thenReturn(false);
- when(jiraService.projectExists(anyString(), anyString())).thenReturn(false);
+ when(projectExistenceService.isProjectFound("AB0042")).thenReturn(false);
String result = tested.generateProjectKey("AB%04d");
diff --git a/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectExistenceServiceImplTest.java b/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectExistenceServiceImplTest.java
new file mode 100644
index 0000000..aaabce3
--- /dev/null
+++ b/service-projects/src/test/java/org/opendevstack/apiservice/serviceproject/service/impl/ProjectExistenceServiceImplTest.java
@@ -0,0 +1,137 @@
+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.serviceproject.exception.ProjectExistenceServiceException;
+import org.opendevstack.apiservice.serviceproject.model.ProjectResponse;
+import org.opendevstack.apiservice.serviceproject.service.ProjectService;
+
+import java.util.Collections;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+class ProjectExistenceServiceImplTest {
+
+ @Mock
+ private BitbucketService bitbucketService;
+ @Mock
+ private JiraService jiraService;
+ @Mock
+ private OpenshiftService openshiftService;
+ @Mock
+ private ProjectService projectService;
+
+ private ProjectExistenceServiceImpl sut;
+
+ @BeforeEach
+ void setUp() throws Exception {
+ MockitoAnnotations.openMocks(this).close();
+ sut = new ProjectExistenceServiceImpl(bitbucketService, jiraService, openshiftService, projectService);
+ }
+
+ @Test
+ void is_project_found_returns_true_when_project_exists_in_db() throws Exception {
+ when(projectService.getProject("KEY1")).thenReturn(new ProjectResponse());
+
+ boolean result = sut.isProjectFound("KEY1");
+ assertTrue(result);
+ verify(projectService).getProject("KEY1");
+ verifyNoInteractions(bitbucketService, jiraService, openshiftService);
+ }
+
+ @Test
+ void is_project_found_returns_true_when_project_exists_in_bitbucket() throws Exception {
+ when(projectService.getProject("KEY2")).thenReturn(null);
+ when(bitbucketService.getAvailableInstances()).thenReturn(Set.of("inst1"));
+ when(bitbucketService.projectExists("inst1", "KEY2")).thenReturn(true);
+
+ boolean result = sut.isProjectFound("KEY2");
+ assertTrue(result);
+ verify(bitbucketService).projectExists("inst1", "KEY2");
+ }
+
+ @Test
+ void is_project_found_returns_true_when_project_exists_in_jira() throws Exception {
+ when(projectService.getProject("KEY3")).thenReturn(null);
+ when(bitbucketService.getAvailableInstances()).thenReturn(Collections.emptySet());
+ when(jiraService.getAvailableInstances()).thenReturn(Set.of("jira1"));
+ when(jiraService.projectExists("jira1", "KEY3")).thenReturn(true);
+ boolean result = sut.isProjectFound("KEY3");
+ assertTrue(result);
+ verify(jiraService).projectExists("jira1", "KEY3");
+ }
+
+ @Test
+ void is_project_found_returns_true_when_project_exists_in_openshift() throws Exception {
+ when(projectService.getProject("KEY4")).thenReturn(null);
+ when(bitbucketService.getAvailableInstances()).thenReturn(Collections.emptySet());
+ when(jiraService.getAvailableInstances()).thenReturn(Collections.emptySet());
+ when(openshiftService.getAvailableInstances()).thenReturn(Set.of("ocp1"));
+ when(openshiftService.projectExists("ocp1", "KEY4")).thenReturn(true);
+
+ boolean result = sut.isProjectFound("KEY4");
+ assertTrue(result);
+ verify(openshiftService).projectExists("ocp1", "KEY4");
+ }
+
+ @Test
+ void is_project_found_returns_false_when_project_not_found_anywhere() throws Exception {
+ when(projectService.getProject("KEY5")).thenReturn(null);
+ when(bitbucketService.getAvailableInstances()).thenReturn(Set.of("inst1"));
+ when(bitbucketService.projectExists("inst1", "KEY5")).thenReturn(false);
+ when(jiraService.getAvailableInstances()).thenReturn(Set.of("jira1"));
+ when(jiraService.projectExists("jira1", "KEY5")).thenReturn(false);
+ when(openshiftService.getAvailableInstances()).thenReturn(Set.of("ocp1"));
+
+ when(openshiftService.projectExists("ocp1", "KEY5")).thenReturn(false);
+ boolean result = sut.isProjectFound("KEY5");
+ assertFalse(result);
+ }
+
+ @Test
+ void is_project_found_throws_exception_when_bitbucket_fails() {
+ when(projectService.getProject("KEY6")).thenReturn(null);
+ when(bitbucketService.getAvailableInstances()).thenThrow(new RuntimeException("fail"));
+ assertThrows(ProjectExistenceServiceException.class, () -> sut.isProjectFound("KEY6"));
+
+ assertThrows(ProjectExistenceServiceException.class, () -> {
+ sut.isProjectFound("KEY6");
+ });
+ }
+
+ @Test
+ void is_project_found_throws_exception_when_jira_fails() {
+ when(projectService.getProject("KEY7")).thenReturn(null);
+ when(bitbucketService.getAvailableInstances()).thenReturn(Collections.emptySet());
+ when(jiraService.getAvailableInstances()).thenThrow(new RuntimeException("fail"));
+
+ assertThrows(ProjectExistenceServiceException.class, () -> sut.isProjectFound("KEY7"));
+
+ assertThrows(ProjectExistenceServiceException.class, () -> {
+ sut.isProjectFound("KEY7");
+ });
+ }
+
+ @Test
+ void is_project_found_throws_exception_when_openshift_fails() {
+ when(projectService.getProject("KEY8")).thenReturn(null);
+ when(bitbucketService.getAvailableInstances()).thenReturn(Collections.emptySet());
+ when(jiraService.getAvailableInstances()).thenReturn(Collections.emptySet());
+ when(openshiftService.getAvailableInstances()).thenThrow(new RuntimeException("fail"));
+
+ assertThrows(ProjectExistenceServiceException.class, () -> {
+ sut.isProjectFound("KEY8");
+ });
+ }
+}
\ 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
index 827ba41..51adb00 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
@@ -4,16 +4,11 @@
import org.junit.jupiter.api.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
-import org.opendevstack.apiservice.externalservice.bitbucket.service.BitbucketService;
-import org.opendevstack.apiservice.externalservice.jira.service.JiraService;
-import org.opendevstack.apiservice.externalservice.ocp.service.OpenshiftService;
import org.opendevstack.apiservice.persistence.entity.ProjectEntity;
import org.opendevstack.apiservice.persistence.repository.ProjectRepository;
import org.opendevstack.apiservice.serviceproject.mapper.ProjectResponseMapper;
-import org.opendevstack.apiservice.serviceproject.model.ProjectRequest;
import org.opendevstack.apiservice.serviceproject.model.ProjectResponse;
import org.opendevstack.apiservice.serviceproject.model.Status;
-import org.opendevstack.apiservice.serviceproject.service.GenerateProjectKeyService;
import java.util.Optional;
import java.util.UUID;
@@ -29,18 +24,6 @@
class ProjectServiceImplTest {
- @Mock
- private OpenshiftService openshiftService;
-
- @Mock
- private BitbucketService bitbucketService;
-
- @Mock
- private JiraService jiraService;
-
- @Mock
- private GenerateProjectKeyService generateProjectKeyService;
-
@Mock
private ProjectRepository projectRepository;
@@ -53,10 +36,6 @@ class ProjectServiceImplTest {
void setUp() {
MockitoAnnotations.openMocks(this);
projectService = new ProjectServiceImpl(
- openshiftService,
- bitbucketService,
- jiraService,
- generateProjectKeyService,
projectRepository,
projectResponseMapper
);
@@ -89,10 +68,8 @@ void get_project_returns_response_when_project_exists() {
when(projectRepository.findByProjectKeyIgnoreCase(projectKey)).thenReturn(Optional.of(projectEntity));
when(projectResponseMapper.toCreateProjectResponse(projectEntity)).thenReturn(expectedResponse);
-
ProjectResponse result = projectService.getProject(projectKey);
-
assertNotNull(result);
assertEquals(projectKey, result.getProjectKey());
@@ -107,33 +84,14 @@ void get_project_returns_null_when_project_does_not_exist() {
String projectKey = "NON-EXISTING";
when(projectRepository.findByProjectKeyIgnoreCase(projectKey)).thenReturn(Optional.empty());
-
ProjectResponse result = projectService.getProject(projectKey);
-
assertNull(result);
verify(projectRepository).findByProjectKeyIgnoreCase(projectKey);
verify(projectResponseMapper, never()).toCreateProjectResponse(any());
}
- @Test
- void create_project_returns_empty_response() {
-
- ProjectRequest request = new ProjectRequest();
- request.setProjectKey("NEW-PROJECT");
- request.setProjectKeyPattern("NEW%06d");
- request.setProjectName("New Project");
- request.setProjectDescription("New test project");
-
-
- ProjectResponse result = projectService.createProject(request);
-
-
- assertNotNull(result);
- assertNull(result.getProjectKey());
- }
-
@Test
void get_project_propagates_repository_exception() {
@@ -150,10 +108,8 @@ void get_project_propagates_repository_exception() {
void get_project_returns_null_when_project_key_is_null() {
when(projectRepository.findByProjectKeyIgnoreCase(null)).thenReturn(Optional.empty());
-
ProjectResponse result = projectService.getProject(null);
-
assertNull(result);
verify(projectRepository).findByProjectKeyIgnoreCase(null);