Skip to content

Commit bcbbb11

Browse files
authored
Add ProjectAlreadyExistsException and enhance project creation validations (#19)
…tion
1 parent 60c5067 commit bcbbb11

14 files changed

Lines changed: 216 additions & 42 deletions

File tree

api-project/src/main/java/org/opendevstack/apiservice/project/controller/ProjectResponseFactory.java

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,6 @@ public final class ProjectResponseFactory {
88
private ProjectResponseFactory() {
99
}
1010

11-
public static CreateProjectResponse conflict(String message, String location) {
12-
return error(
13-
ErrorKey.PROJECT_ALREADY_EXISTS.getMessage(),
14-
ErrorKey.PROJECT_ALREADY_EXISTS.getKey(),
15-
message, location);
16-
}
17-
18-
public static CreateProjectResponse projectKeyGenerationFailed(String location) {
19-
return error(ErrorKey.INTERNAL_ERROR.getMessage(),
20-
"PROJECT_KEY_GENERATION_FAILED",
21-
"Failed to generate a unique project key.",
22-
location);
23-
}
24-
2511
public static CreateProjectResponse notFound(String projectKey, String location) {
2612
return error(
2713
ErrorKey.PROJECT_NOT_FOUND.getMessage(),
@@ -31,22 +17,6 @@ public static CreateProjectResponse notFound(String projectKey, String location)
3117
);
3218
}
3319

34-
public static CreateProjectResponse internalError(String location) {
35-
return error(
36-
ErrorKey.INTERNAL_ERROR.getMessage(),
37-
ErrorKey.INTERNAL_ERROR.getKey(),
38-
"An error occurred while processing the request.",
39-
location);
40-
}
41-
42-
public static CreateProjectResponse internalError(String location, String message) {
43-
return error(
44-
ErrorKey.INTERNAL_ERROR.getMessage(),
45-
ErrorKey.INTERNAL_ERROR.getKey(),
46-
message,
47-
location);
48-
}
49-
5020
private static CreateProjectResponse error(String error, String errorKey, String message, String location) {
5121
CreateProjectResponse response = new CreateProjectResponse();
5222
response.setError(error);

api-project/src/main/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandler.java

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.opendevstack.apiservice.project.controller.ProjectController;
66
import org.opendevstack.apiservice.project.exception.ClientAppNotRegisteredException;
77
import org.opendevstack.apiservice.project.exception.ErrorKey;
8+
import org.opendevstack.apiservice.project.exception.ProjectAlreadyExistsException;
89
import org.opendevstack.apiservice.project.exception.ProjectCreationException;
910
import org.opendevstack.apiservice.project.exception.ProjectValidationException;
1011
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
@@ -81,10 +82,24 @@ public ResponseEntity<CreateProjectResponse> handleValidationException(ProjectVa
8182
response.setLocation(ProjectController.API_BASE_PATH);
8283
response.setError(HttpStatus.BAD_REQUEST.getReasonPhrase());
8384
response.setErrorKey(errorKey.getKey());
84-
response.setMessage(ex.getMessage()); // Needs to get the full message with additional info provided.
85+
response.setMessage(ex.getMessage());
8586
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
8687
}
8788

89+
@ExceptionHandler(ProjectAlreadyExistsException.class)
90+
public ResponseEntity<CreateProjectResponse> handleProjectAlreadyExistsException(ProjectAlreadyExistsException ex) {
91+
log.warn("Validation error: {}", ex.getMessage());
92+
ErrorKey errorKey = ex.getErrorKey() != null
93+
? ex.getErrorKey()
94+
: ErrorKey.PROJECT_ALREADY_EXISTS;
95+
CreateProjectResponse response = new CreateProjectResponse();
96+
response.setLocation(ProjectController.API_BASE_PATH);
97+
response.setError(HttpStatus.CONFLICT.getReasonPhrase());
98+
response.setErrorKey(errorKey.getKey());
99+
response.setMessage(errorKey.getMessage());
100+
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
101+
}
102+
88103
@ExceptionHandler(ProjectCreationException.class)
89104
public ResponseEntity<CreateProjectResponse> handleProjectCreationException(
90105
ProjectCreationException ex) {
@@ -114,9 +129,9 @@ public ResponseEntity<CreateProjectResponse> handleHttpMessageNotReadableExcepti
114129
log.error("Unexpected error: {}", ex.getMessage(), ex);
115130
CreateProjectResponse response = new CreateProjectResponse();
116131
response.setLocation(ProjectController.API_BASE_PATH);
117-
response.setError(ErrorKey.BAD_REQUEST_BODY.getMessage());
118-
response.setErrorKey(ErrorKey.BAD_REQUEST_BODY.getKey());
119-
response.setMessage("An error occurred while processing the request.");
132+
response.setError(ErrorKey.COMPONENT_PARAM_INVALID_FORMAT.getMessage());
133+
response.setErrorKey(ErrorKey.COMPONENT_PARAM_INVALID_FORMAT.getKey());
134+
response.setMessage("Request body should be a valid json.");
120135
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
121136
}
122137

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package org.opendevstack.apiservice.project.exception;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public class ProjectAlreadyExistsException extends RuntimeException {
7+
8+
private ErrorKey errorKey;
9+
10+
public ProjectAlreadyExistsException() {
11+
super();
12+
}
13+
14+
public ProjectAlreadyExistsException(ErrorKey errorKey) {
15+
super(errorKey.getMessage());
16+
this.errorKey = errorKey;
17+
}
18+
}
19+

api-project/src/main/java/org/opendevstack/apiservice/project/facade/impl/ProjectCreationCommandBuilder.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.opendevstack.apiservice.persistence.entity.ClientAppEntity;
77
import org.opendevstack.apiservice.persistence.entity.ClientAppProjectFlavorEntity;
88
import org.opendevstack.apiservice.project.exception.ErrorKey;
9+
import org.opendevstack.apiservice.project.exception.ProjectAlreadyExistsException;
910
import org.opendevstack.apiservice.project.exception.ProjectCreationException;
1011
import org.opendevstack.apiservice.project.exception.ProjectValidationException;
1112
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
@@ -15,6 +16,7 @@
1516
import org.opendevstack.apiservice.serviceproject.service.ProjectExistenceService;
1617
import org.springframework.stereotype.Component;
1718

19+
import java.text.MessageFormat;
1820
import java.util.Arrays;
1921
import java.util.List;
2022
import java.util.UUID;
@@ -37,7 +39,7 @@ public ProjectCreationCommand build(CreateProjectRequest request, ClientAppEntit
3739
String x2account = firstNonBlank(request.getX2OdsAccount(), flavor.getServiceAccount());
3840
String location = firstNonBlank(request.getLocation(), flavor.getLocation());
3941
String projectKey = resolveProjectKey(request.getProjectKey(), flavor);
40-
String projectName = firstNonBlank(request.getProjectName(), projectKey);
42+
String projectName = resolveProjectName(request.getProjectName(), projectKey);
4143
String projectDescription = firstNonBlank(request.getProjectDescription(), "project " + projectFlavor);
4244

4345
return new ProjectCreationCommand(
@@ -91,7 +93,11 @@ private ClientAppProjectFlavorEntity resolveByConfigurationItem(
9193
if (matchingFlavors.size() != 1) {
9294
log.warn("ConfigItem '{}' does not match exactly one flavor for clientApp '{}'",
9395
configurationItem, clientId);
94-
throw new ProjectValidationException(ErrorKey.INVALID_CONFIG_ITEM);
96+
String message = MessageFormat.
97+
format("Not exists a project flavor configured for the Config Item {0}. " +
98+
"To create a project under {0} the projectFlavor parameter is mandatory.",
99+
configurationItem);
100+
throw new ProjectValidationException(ErrorKey.INVALID_CONFIG_ITEM, message);
95101
}
96102

97103
ClientAppProjectFlavorEntity matchedFlavor = matchingFlavors.getFirst();
@@ -112,7 +118,7 @@ private String resolveProjectKey(String existingProjectKey, ClientAppProjectFlav
112118
return existingProjectKey;
113119
}
114120

115-
throw new ProjectValidationException(ErrorKey.PROJECT_ALREADY_EXISTS);
121+
throw new ProjectAlreadyExistsException(ErrorKey.PROJECT_ALREADY_EXISTS);
116122
}
117123

118124
String pattern = flavor.getProjectKeyPattern();
@@ -138,5 +144,19 @@ private boolean isAllowedConfigItem(String configurationItem, ClientAppProjectFl
138144
private String firstNonBlank(String preferred, String fallback) {
139145
return Strings.isNotEmpty(preferred) ? preferred : fallback;
140146
}
147+
148+
private String resolveProjectName(String preferred, String fallback) {
149+
if (!Strings.isEmpty(preferred)) {
150+
try {
151+
if (projectExistenceService.isProjectFoundByName(preferred)) {
152+
throw new ProjectAlreadyExistsException(ErrorKey.PROJECT_SAME_PROJECT_NAME_ALREADY_EXISTS);
153+
}
154+
} catch (ProjectExistenceServiceException e) {
155+
throw new ProjectCreationException("Error checking if project name already exists: " + e.getMessage(), e);
156+
}
157+
}
158+
159+
return firstNonBlank(preferred, fallback);
160+
}
141161
}
142162

api-project/src/test/java/org/opendevstack/apiservice/project/controller/advice/ProjectExceptionHandlerTest.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@
1111
import org.opendevstack.apiservice.project.controller.ProjectController;
1212
import org.opendevstack.apiservice.project.exception.ClientAppNotRegisteredException;
1313
import org.opendevstack.apiservice.project.exception.ErrorKey;
14+
import org.opendevstack.apiservice.project.exception.ProjectAlreadyExistsException;
1415
import org.opendevstack.apiservice.project.exception.ProjectCreationException;
1516
import org.opendevstack.apiservice.project.exception.ProjectValidationException;
1617
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
1718
import org.opendevstack.apiservice.project.model.CreateProjectResponse;
1819
import org.springframework.core.MethodParameter;
1920
import org.springframework.http.HttpStatus;
2021
import org.springframework.http.ResponseEntity;
22+
import org.springframework.http.converter.HttpMessageNotReadableException;
2123
import org.springframework.validation.BeanPropertyBindingResult;
2224
import org.springframework.validation.FieldError;
2325
import org.springframework.web.bind.MethodArgumentNotValidException;
@@ -287,4 +289,32 @@ void handle_generic_exception_returns_internal_server_error() {
287289
assertNull(result.getBody().getStatus());
288290
assertNull(result.getBody().getErrorDescription());
289291
}
292+
293+
@Test
294+
void handle_http_message_not_readable_exception_returns_bad_request_with_error_key_017() {
295+
HttpMessageNotReadableException exception = new HttpMessageNotReadableException("Malformed JSON", new RuntimeException("cause"));
296+
297+
ResponseEntity<CreateProjectResponse> result = sut.handleHttpMessageNotReadableException(exception);
298+
299+
assertEquals(HttpStatus.BAD_REQUEST, result.getStatusCode());
300+
assertNotNull(result.getBody());
301+
assertEquals(ProjectController.API_BASE_PATH, result.getBody().getLocation());
302+
assertEquals("Bad Request", result.getBody().getError());
303+
assertEquals("017", result.getBody().getErrorKey());
304+
assertEquals("Request body should be a valid json.", result.getBody().getMessage());
305+
}
306+
307+
@Test
308+
void handle_project_already_exists_exception_returns_conflict() {
309+
ProjectAlreadyExistsException exception = new ProjectAlreadyExistsException();
310+
311+
ResponseEntity<CreateProjectResponse> result = sut.handleProjectAlreadyExistsException(exception);
312+
313+
assertEquals(HttpStatus.CONFLICT, result.getStatusCode());
314+
assertNotNull(result.getBody());
315+
assertEquals(ProjectController.API_BASE_PATH, result.getBody().getLocation());
316+
assertEquals("Conflict", result.getBody().getError());
317+
assertEquals("025", result.getBody().getErrorKey());
318+
assertEquals("Project already exists", result.getBody().getMessage());
319+
}
290320
}

api-project/src/test/java/org/opendevstack/apiservice/project/facade/impl/ProjectCreationCommandBuilderTest.java

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.opendevstack.apiservice.persistence.entity.ClientAppEntity;
99
import org.opendevstack.apiservice.persistence.entity.ClientAppProjectFlavorEntity;
1010
import org.opendevstack.apiservice.project.exception.ErrorKey;
11+
import org.opendevstack.apiservice.project.exception.ProjectAlreadyExistsException;
1112
import org.opendevstack.apiservice.project.exception.ProjectCreationException;
1213
import org.opendevstack.apiservice.project.exception.ProjectValidationException;
1314
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
@@ -97,14 +98,14 @@ void build_generates_project_key_when_request_project_key_is_null() throws Proje
9798
}
9899

99100
@Test
100-
void build_throws_validation_exception_when_project_key_already_exists() throws ProjectExistenceServiceException {
101+
void build_throws_project_already_exists_exception_when_project_key_already_exists() throws ProjectExistenceServiceException {
101102
ClientAppProjectFlavorEntity flavor = build_flavor("DLSS", "CI-001", new String[] {}, "eu", "owner1");
102103
ClientAppEntity clientApp = build_client_app(List.of(flavor));
103104
CreateProjectRequest request = build_request("DLSS", null, "KEY01");
104105

105106
when(projectExistenceService.isProjectFound("KEY01")).thenReturn(true);
106107

107-
ProjectValidationException ex = assertThrows(ProjectValidationException.class,
108+
ProjectAlreadyExistsException ex = assertThrows(ProjectAlreadyExistsException.class,
108109
() -> sut.build(request, clientApp));
109110
assertEquals(ErrorKey.PROJECT_ALREADY_EXISTS, ex.getErrorKey());
110111
}
@@ -144,6 +145,33 @@ void build_throws_project_key_generation_exception_when_generation_fails() throw
144145
assertThrows(ProjectCreationException.class, () -> sut.build(request, clientApp));
145146
}
146147

148+
@Test
149+
void build_throws_project_already_exists_exception_when_project_name_already_exists() throws Exception {
150+
ClientAppProjectFlavorEntity flavor = build_flavor("DLSS", "CI-001", new String[] {}, "eu", "owner1");
151+
ClientAppEntity clientApp = build_client_app(List.of(flavor));
152+
CreateProjectRequest request = build_request("DLSS", null, "KEY01");
153+
request.setProjectName("Existing Project");
154+
155+
when(projectExistenceService.isProjectFound("KEY01")).thenReturn(false);
156+
when(projectExistenceService.isProjectFoundByName("Existing Project")).thenReturn(true);
157+
158+
assertThrows(ProjectAlreadyExistsException.class, () -> sut.build(request, clientApp));
159+
}
160+
161+
@Test
162+
void build_throws_project_creation_exception_when_project_name_check_fails() throws Exception {
163+
ClientAppProjectFlavorEntity flavor = build_flavor("DLSS", "CI-001", new String[] {}, "eu", "owner1");
164+
ClientAppEntity clientApp = build_client_app(List.of(flavor));
165+
CreateProjectRequest request = build_request("DLSS", null, "KEY01");
166+
request.setProjectName("Any Project");
167+
168+
when(projectExistenceService.isProjectFound("KEY01")).thenReturn(false);
169+
when(projectExistenceService.isProjectFoundByName("Any Project"))
170+
.thenThrow(new ProjectExistenceServiceException("lookup failed"));
171+
172+
assertThrows(ProjectCreationException.class, () -> sut.build(request, clientApp));
173+
}
174+
147175
private CreateProjectRequest build_request(String flavor, String configItem, String projectKey) {
148176
CreateProjectRequest request = new CreateProjectRequest();
149177
request.setProjectKey(projectKey);

persistence/src/main/java/org/opendevstack/apiservice/persistence/repository/ProjectRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
public interface ProjectRepository extends JpaRepository<ProjectEntity, UUID> {
2323

2424
Optional<ProjectEntity> findByProjectKeyIgnoreCase(String projectKey);
25+
26+
List<ProjectEntity> findByProjectNameIgnoreCase(String projectName);
2527

2628
List<ProjectEntity> findByDeletedFalse();
2729

service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/mapper/ProjectResponseMapper.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package org.opendevstack.apiservice.serviceproject.mapper;
22

33
import java.util.Arrays;
4+
import java.util.List;
5+
46
import org.mapstruct.Mapper;
57
import org.mapstruct.Mapping;
8+
import org.mapstruct.IterableMapping;
69
import org.mapstruct.Named;
710
import org.opendevstack.apiservice.persistence.entity.ProjectEntity;
811
import org.opendevstack.apiservice.serviceproject.model.ProjectResponse;
@@ -13,8 +16,12 @@ public interface ProjectResponseMapper {
1316

1417
@Mapping(source = "status", target = "status", qualifiedByName = "mapStatus")
1518
@Mapping(source = "id", target = "projectId")
19+
@Named("mapEntityToResponse")
1620
ProjectResponse toCreateProjectResponse(ProjectEntity entity);
1721

22+
@IterableMapping(qualifiedByName = "mapEntityToResponse")
23+
List<ProjectResponse> toCreateProjectResponse(List<ProjectEntity> entities);
24+
1825
@Named("mapStatus")
1926
default Status mapStatus(String value) {
2027
if (value == null) {

service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectExistenceService.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@
55
public interface ProjectExistenceService {
66

77
boolean isProjectFound(String projectKey) throws ProjectExistenceServiceException;
8+
9+
boolean isProjectFoundByName(String projecName) throws ProjectExistenceServiceException;
810
}

service-projects/src/main/java/org/opendevstack/apiservice/serviceproject/service/ProjectService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
import org.opendevstack.apiservice.serviceproject.model.ProjectRequest;
44
import org.opendevstack.apiservice.serviceproject.model.ProjectResponse;
55

6+
import java.util.List;
7+
68
public interface ProjectService {
79

810
ProjectResponse saveProject(ProjectRequest request);
911

1012
ProjectResponse getProject(String projectKey);
13+
14+
List<ProjectResponse> findProjectsByName(String projectName);
1115
}
1216

0 commit comments

Comments
 (0)