Skip to content

Commit 3058df6

Browse files
committed
Feature/add global exception handling and validation error responses
1 parent 9e4b4ce commit 3058df6

4 files changed

Lines changed: 171 additions & 57 deletions

File tree

api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/controller/advice/ProjectUserExceptionHandler.java renamed to api-project-users/src/main/java/org/opendevstack/apiservice/projectusers/exception/GlobalExceptionHandler.java

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,40 @@
1-
package org.opendevstack.apiservice.projectusers.controller.advice;
1+
package org.opendevstack.apiservice.projectusers.exception;
22

3+
import org.opendevstack.apiservice.projectusers.controller.ProjectUserController;
4+
import org.opendevstack.apiservice.projectusers.model.ValidationErrorResponse;
5+
import org.opendevstack.apiservice.projectusers.model.BaseApiResponse;
6+
import org.opendevstack.apiservice.projectusers.model.FieldError;
37
import com.fasterxml.jackson.databind.JsonMappingException;
48
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
59
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
610
import jakarta.validation.ConstraintViolation;
711
import jakarta.validation.ConstraintViolationException;
812
import lombok.extern.slf4j.Slf4j;
9-
import org.opendevstack.apiservice.externalservice.aap.exception.AutomationPlatformException;
10-
import org.opendevstack.apiservice.projectusers.controller.ProjectUserController;
11-
import org.opendevstack.apiservice.projectusers.exception.ErrorCodes;
12-
import org.opendevstack.apiservice.projectusers.exception.ErrorMessages;
13-
import org.opendevstack.apiservice.projectusers.exception.InvalidRoleException;
14-
import org.opendevstack.apiservice.projectusers.exception.ProjectNotFoundException;
15-
import org.opendevstack.apiservice.projectusers.exception.ProjectUserException;
16-
import org.opendevstack.apiservice.projectusers.exception.UserNotAuthenticatedException;
17-
import org.opendevstack.apiservice.projectusers.exception.UserNotAuthorizedException;
18-
import org.opendevstack.apiservice.projectusers.exception.UserNotFoundException;
19-
import org.opendevstack.apiservice.projectusers.model.BaseApiResponse;
20-
import org.opendevstack.apiservice.projectusers.model.FieldError;
21-
import org.opendevstack.apiservice.projectusers.model.ValidationErrorResponse;
13+
2214
import org.springframework.http.HttpStatus;
2315
import org.springframework.http.ResponseEntity;
2416
import org.springframework.http.converter.HttpMessageNotReadableException;
2517
import org.springframework.web.bind.MethodArgumentNotValidException;
2618
import org.springframework.web.bind.MissingPathVariableException;
2719
import org.springframework.web.bind.MissingServletRequestParameterException;
20+
import org.springframework.web.bind.annotation.ControllerAdvice;
2821
import org.springframework.web.bind.annotation.ExceptionHandler;
29-
import org.springframework.web.bind.annotation.RestControllerAdvice;
3022
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
23+
import org.opendevstack.apiservice.externalservice.aap.exception.AutomationPlatformException;
3124

3225
import java.util.ArrayList;
3326
import java.util.List;
3427
import java.util.stream.Collectors;
3528

29+
3630
/**
37-
* Exception handler for the Project Users API.
31+
* Global exception handler for the Project Users API.
3832
* Provides comprehensive error handling with detailed validation error
3933
* messages.
4034
*/
4135
@Slf4j
42-
@RestControllerAdvice(assignableTypes = ProjectUserController.class)
43-
public class ProjectUserExceptionHandler {
36+
@ControllerAdvice(assignableTypes = ProjectUserController.class)
37+
public class GlobalExceptionHandler {
4438

4539
/**
4640
* Handles validation errors from @Valid annotations on request bodies.
@@ -53,6 +47,7 @@ public ResponseEntity<ValidationErrorResponse> handleMethodArgumentNotValidExcep
5347

5448
List<FieldError> fieldErrors = new ArrayList<>();
5549

50+
// Field validation errors
5651
for (org.springframework.validation.FieldError error : ex.getBindingResult().getFieldErrors()) {
5752
String fieldName = error.getField();
5853
String errorMessage = error.getDefaultMessage();
@@ -67,6 +62,7 @@ public ResponseEntity<ValidationErrorResponse> handleMethodArgumentNotValidExcep
6762
fieldErrors.add(fieldError);
6863
}
6964

65+
// Global validation errors
7066
ex.getBindingResult().getGlobalErrors().forEach(error -> {
7167
FieldError fieldError = new FieldError();
7268
fieldError.setField("object");
@@ -381,5 +377,4 @@ private String getFieldPath(List<JsonMappingException.Reference> path) {
381377
.map(ref -> ref.getFieldName() != null ? ref.getFieldName() : "[" + ref.getIndex() + "]")
382378
.collect(Collectors.joining("."));
383379
}
384-
}
385-
380+
}

api-project-users/src/test/java/org/opendevstack/apiservice/projectusers/controller/advice/ProjectUserExceptionHandlerTest.java renamed to api-project-users/src/test/java/org/opendevstack/apiservice/projectusers/exception/GlobalExceptionHandlerTest.java

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,42 @@
1-
package org.opendevstack.apiservice.projectusers.controller.advice;
2-
import org.junit.jupiter.api.AfterEach;
3-
import org.junit.jupiter.api.BeforeEach;
4-
import org.junit.jupiter.api.Test;
5-
import org.mockito.MockitoAnnotations;
1+
package org.opendevstack.apiservice.projectusers.exception;
2+
63
import org.opendevstack.apiservice.projectusers.controller.ProjectUserController;
74
import org.opendevstack.apiservice.projectusers.model.AddUserToProjectRequest;
85
import org.opendevstack.apiservice.projectusers.model.ValidationErrorResponse;
9-
import org.springframework.core.MethodParameter;
6+
import org.junit.jupiter.api.BeforeEach;
7+
import org.junit.jupiter.api.Test;
108
import org.springframework.http.HttpStatus;
119
import org.springframework.http.ResponseEntity;
1210
import org.springframework.validation.BeanPropertyBindingResult;
1311
import org.springframework.validation.FieldError;
1412
import org.springframework.web.bind.MethodArgumentNotValidException;
13+
import org.springframework.core.MethodParameter;
14+
1515
import java.util.List;
16-
import static org.junit.jupiter.api.Assertions.assertEquals;
17-
import static org.junit.jupiter.api.Assertions.assertFalse;
18-
import static org.junit.jupiter.api.Assertions.assertNotNull;
19-
import static org.junit.jupiter.api.Assertions.assertTrue;
20-
import static org.junit.jupiter.api.Assertions.fail;
16+
17+
import static org.junit.jupiter.api.Assertions.*;
18+
2119
/**
22-
* Unit test class for the ProjectUserExceptionHandler to verify improved validation
20+
* Unit test class for the GlobalExceptionHandler to verify improved validation
2321
* error messages.
2422
*/
25-
class ProjectUserExceptionHandlerTest {
26-
private ProjectUserExceptionHandler sut;
27-
private AutoCloseable mocks;
23+
class GlobalExceptionHandlerTest {
24+
25+
private GlobalExceptionHandler exceptionHandler;
26+
2827
@BeforeEach
2928
void setUp() {
30-
mocks = MockitoAnnotations.openMocks(this);
31-
sut = new ProjectUserExceptionHandler();
32-
}
33-
@AfterEach
34-
void tearDown() throws Exception {
35-
mocks.close();
29+
exceptionHandler = new GlobalExceptionHandler();
3630
}
31+
3732
@Test
38-
void handle_method_argument_not_valid_exception_returns_bad_request_with_field_errors() {
39-
// GIVEN
33+
void testValidationErrorHandling() {
34+
// Create a mock MethodArgumentNotValidException with validation errors
35+
// Target object representing the @RequestBody argument
4036
AddUserToProjectRequest target = new AddUserToProjectRequest();
4137
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(target, "addUserToProjectRequest");
38+
39+
// Add field errors for required fields
4240
bindingResult.addError(new FieldError("addUserToProjectRequest", "environment", null, false, null, null,
4341
"Environment cannot be blank"));
4442
bindingResult.addError(
@@ -47,32 +45,45 @@ void handle_method_argument_not_valid_exception_returns_bad_request_with_field_e
4745
"Account cannot be blank"));
4846
bindingResult.addError(
4947
new FieldError("addUserToProjectRequest", "role", null, false, null, null, "Role cannot be null"));
48+
49+
// Create a MethodParameter referencing the controller method's @RequestBody
50+
// parameter
5051
MethodParameter methodParameter;
5152
try {
5253
methodParameter = new MethodParameter(
5354
ProjectUserController.class.getMethod(
5455
"triggerMembershipRequest", String.class, AddUserToProjectRequest.class),
55-
1);
56+
1 // index of AddUserToProjectRequest parameter
57+
);
5658
} catch (NoSuchMethodException e) {
5759
fail("Failed to reflect controller method for test: " + e.getMessage());
58-
return;
60+
return; // unreachable, but required for compilation
5961
}
62+
6063
MethodArgumentNotValidException exception = new MethodArgumentNotValidException(methodParameter, bindingResult);
61-
// WHEN
62-
ResponseEntity<ValidationErrorResponse> response = sut.handleMethodArgumentNotValidException(exception);
63-
// THEN
64+
65+
// Test the exception handler
66+
ResponseEntity<ValidationErrorResponse> response = exceptionHandler
67+
.handleMethodArgumentNotValidException(exception);
68+
69+
// Verify response
6470
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
6571
assertNotNull(response.getBody());
72+
6673
ValidationErrorResponse errorResponse = response.getBody();
6774
assertFalse(errorResponse.getSuccess());
6875
assertEquals("PROJECT_USER_ERROR", errorResponse.getErrorCode());
6976
assertNotNull(errorResponse.getFieldErrors());
7077
assertEquals(4, errorResponse.getFieldErrors().size());
78+
79+
// Check specific field errors
7180
List<org.opendevstack.apiservice.projectusers.model.FieldError> fieldErrors = errorResponse.getFieldErrors();
7281
assertTrue(fieldErrors.stream().anyMatch(error -> "environment".equals(error.getField())));
7382
assertTrue(fieldErrors.stream().anyMatch(error -> "user".equals(error.getField())));
7483
assertTrue(fieldErrors.stream().anyMatch(error -> "account".equals(error.getField())));
7584
assertTrue(fieldErrors.stream().anyMatch(error -> "role".equals(error.getField())));
85+
86+
// Verify expected format is provided for each field
7687
fieldErrors.forEach(fieldError -> {
7788
assertNotNull(fieldError.getField());
7889
assertNotNull(fieldError.getMessage());
@@ -82,14 +93,15 @@ void handle_method_argument_not_valid_exception_returns_bad_request_with_field_e
8293
}
8394
});
8495
}
96+
8597
@Test
86-
void handle_generic_exception_returns_internal_server_error() {
87-
// GIVEN
98+
void testGenericExceptionHandling() {
99+
// Test generic exception handling
88100
Exception exception = new RuntimeException("Unexpected error");
89-
// WHEN
90-
ResponseEntity<?> response = sut.handleGenericException(exception);
91-
// THEN
101+
102+
ResponseEntity<?> response = exceptionHandler.handleGenericException(exception);
103+
92104
assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, response.getStatusCode());
93105
assertNotNull(response.getBody());
94106
}
95-
}
107+
}

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

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,6 @@ void handle_validation_exception_returns_bad_request_response_for_missing_flavor
9090

9191
@Test
9292
void handle_method_argument_not_valid_exception_returns_bad_request_response_for_request_body_validation_errors() {
93-
// GIVEN
9493
CreateProjectRequest target = new CreateProjectRequest();
9594
BeanPropertyBindingResult bindingResult = new BeanPropertyBindingResult(target, "createProjectRequest");
9695
bindingResult.addError(new FieldError("createProjectRequest", "projectName", null, false, null, null,
@@ -108,10 +107,8 @@ void handle_method_argument_not_valid_exception_returns_bad_request_response_for
108107

109108
MethodArgumentNotValidException exception = new MethodArgumentNotValidException(methodParameter, bindingResult);
110109

111-
// WHEN
112110
ResponseEntity<CreateProjectResponse> result = sut.handleMethodArgumentNotValidException(exception);
113111

114-
// THEN
115112
assertEquals(HttpStatus.BAD_REQUEST, result.getStatusCode());
116113
assertNotNull(result.getBody());
117114
assertEquals(ProjectController.API_BASE_PATH, result.getBody().getLocation());
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package org.opendevstack.apiservice.project.validation;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
import org.opendevstack.apiservice.project.exception.ErrorKey;
6+
import org.opendevstack.apiservice.project.exception.ProjectValidationException;
7+
import org.opendevstack.apiservice.project.model.CreateProjectRequest;
8+
9+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
10+
import static org.junit.jupiter.api.Assertions.assertEquals;
11+
import static org.junit.jupiter.api.Assertions.assertThrows;
12+
13+
class ProjectRequestValidatorTest {
14+
15+
private ProjectRequestValidator sut;
16+
17+
@BeforeEach
18+
void setUp() {
19+
sut = new ProjectRequestValidator();
20+
}
21+
22+
@Test
23+
void validate_throws_exception_when_project_flavor_and_config_item_both_null() {
24+
CreateProjectRequest request = new CreateProjectRequest();
25+
request.setProjectName("Valid Name");
26+
request.setProjectFlavor(null);
27+
request.setConfigurationItem(null);
28+
29+
ProjectValidationException exception = assertThrows(
30+
ProjectValidationException.class,
31+
() -> sut.validate(request)
32+
);
33+
34+
assertEquals(ErrorKey.BAD_REQUEST_FLAVOR_CONFIG_ITEM, exception.getErrorKey());
35+
}
36+
37+
@Test
38+
void validate_throws_exception_when_project_flavor_and_config_item_both_empty() {
39+
CreateProjectRequest request = new CreateProjectRequest();
40+
request.setProjectName("Valid Name");
41+
request.setProjectFlavor("");
42+
request.setConfigurationItem(" ");
43+
44+
ProjectValidationException exception = assertThrows(
45+
ProjectValidationException.class,
46+
() -> sut.validate(request)
47+
);
48+
49+
assertEquals(ErrorKey.BAD_REQUEST_FLAVOR_CONFIG_ITEM, exception.getErrorKey());
50+
}
51+
52+
@Test
53+
void validate_succeeds_when_project_flavor_provided() {
54+
CreateProjectRequest request = new CreateProjectRequest();
55+
request.setProjectName("Valid Name");
56+
request.setProjectFlavor("STANDARD");
57+
request.setConfigurationItem(null);
58+
59+
assertDoesNotThrow(() -> sut.validate(request));
60+
}
61+
62+
@Test
63+
void validate_succeeds_when_config_item_provided() {
64+
CreateProjectRequest request = new CreateProjectRequest();
65+
request.setProjectName("Valid Name");
66+
request.setProjectFlavor(null);
67+
request.setConfigurationItem("JIRA");
68+
69+
assertDoesNotThrow(() -> sut.validate(request));
70+
}
71+
72+
@Test
73+
void validate_succeeds_when_both_flavor_and_config_item_provided() {
74+
CreateProjectRequest request = new CreateProjectRequest();
75+
request.setProjectName("Valid Name");
76+
request.setProjectFlavor("STANDARD");
77+
request.setConfigurationItem("JIRA");
78+
79+
assertDoesNotThrow(() -> sut.validate(request));
80+
}
81+
82+
@Test
83+
void validate_throws_exception_for_invalid_project_key_format() {
84+
CreateProjectRequest request = new CreateProjectRequest();
85+
request.setProjectName("Valid Name");
86+
request.setProjectFlavor("STANDARD");
87+
request.setProjectKey("invalid-key"); // Invalid format
88+
89+
ProjectValidationException exception = assertThrows(
90+
ProjectValidationException.class,
91+
() -> sut.validate(request)
92+
);
93+
94+
assertEquals(ErrorKey.PROJECT_KEY_INVALID_FORMAT, exception.getErrorKey());
95+
}
96+
97+
@Test
98+
void validate_throws_exception_for_invalid_project_name_format() {
99+
CreateProjectRequest request = new CreateProjectRequest();
100+
request.setProjectName("Invalid@Name#");
101+
request.setProjectFlavor("STANDARD");
102+
103+
ProjectValidationException exception = assertThrows(
104+
ProjectValidationException.class,
105+
() -> sut.validate(request)
106+
);
107+
108+
assertEquals(ErrorKey.PROJECT_NAME_INVALID_FORMAT, exception.getErrorKey());
109+
}
110+
}

0 commit comments

Comments
 (0)