Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,18 @@
import static org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR;
import static org.springframework.http.HttpStatus.NOT_FOUND;

import com.wcc.platform.domain.exceptions.ApplicationMenteeWorkflowException;
import com.wcc.platform.domain.exceptions.ContentNotFoundException;
import com.wcc.platform.domain.exceptions.DuplicatedException;
import com.wcc.platform.domain.exceptions.EmailSendException;
import com.wcc.platform.domain.exceptions.ErrorDetails;
import com.wcc.platform.domain.exceptions.ForbiddenException;
import com.wcc.platform.domain.exceptions.InvalidProgramTypeException;
import com.wcc.platform.domain.exceptions.MemberNotFoundException;
import com.wcc.platform.domain.exceptions.MenteeNotSavedException;
import com.wcc.platform.domain.exceptions.MenteeRegistrationLimitException;
import com.wcc.platform.domain.exceptions.MentorNotFoundException;
import com.wcc.platform.domain.exceptions.MentorStatusException;
import com.wcc.platform.domain.exceptions.MentorshipCycleClosedException;
import com.wcc.platform.domain.exceptions.PlatformInternalException;
import com.wcc.platform.domain.exceptions.TemplateValidationException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException;
import com.wcc.platform.domain.exceptions.*;
import com.wcc.platform.repository.file.FileRepositoryException;
Comment thread
dricazenck marked this conversation as resolved.
import jakarta.validation.ConstraintViolationException;
import java.util.NoSuchElementException;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
Expand Down Expand Up @@ -84,7 +74,7 @@ public ResponseEntity<ErrorDetails> handleProgramTypeError(
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}

/** Receive {@link DataAccessException} then return {@link HttpStatus#CONFLICT}. */
/** Receive {@link DataIntegrityViolationException} then return {@link HttpStatus#CONFLICT}. */
@ExceptionHandler(DataIntegrityViolationException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ResponseEntity<ErrorDetails> handleDataAccessException(
Expand Down Expand Up @@ -155,6 +145,19 @@ public ResponseEntity<ErrorDetails> handleMethodArgumentNotValidException(
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}

/** Return 400 Bad Request for malformed JSON payloads. */
@ExceptionHandler(HttpMessageNotReadableException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ErrorDetails> handleHttpMessageNotReadableException(
final HttpMessageNotReadableException ex, final WebRequest request) {
final var errorDetails =
new ErrorDetails(
HttpStatus.BAD_REQUEST.value(),
extractReadableMessage(ex),
request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}

/** Return 403 Forbidden for ForbiddenException. */
@ExceptionHandler(ForbiddenException.class)
public ResponseEntity<ErrorDetails> handleForbiddenException(
Expand All @@ -165,4 +168,47 @@ public ResponseEntity<ErrorDetails> handleForbiddenException(

return new ResponseEntity<>(errorResponse, HttpStatus.FORBIDDEN);
}

private String extractReadableMessage(final HttpMessageNotReadableException ex) {
final var cause = ex.getMostSpecificCause();

if (cause instanceof UnrecognizedPropertyException unrecognizedProperty) {
final var allowedFields =
unrecognizedProperty.getKnownPropertyIds().stream()
.map(String::valueOf)
.sorted()
.collect(Collectors.joining(", "));

return "Unrecognized field '%s' at '%s'. Allowed fields: %s"
.formatted(
unrecognizedProperty.getPropertyName(),
formatPath(unrecognizedProperty.getPath()),
allowedFields);
Comment thread
dricazenck marked this conversation as resolved.
}

return cause.getMessage();
}

private String formatPath(final java.util.List<JsonMappingException.Reference> path) {
if (path.isEmpty()) {
Comment thread
dricazenck marked this conversation as resolved.
return "$";
}

return IntStream.range(0, path.size())
.mapToObj(index -> formatPathReference(path.get(index), index == 0))
.collect(Collectors.joining());
}

private String formatPathReference(
final JsonMappingException.Reference reference, final boolean firstReference) {
if (reference.getFieldName() != null) {
return firstReference ? reference.getFieldName() : "." + reference.getFieldName();
}

if (reference.getIndex() >= 0) {
return "[" + reference.getIndex() + "]";
}

return firstReference ? "$" : "";
}
}
48 changes: 24 additions & 24 deletions src/main/resources/init-data/mentorshipPage.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,59 +9,59 @@
"mentorSection": {
"title": "Become a Mentor",
"description": "You should apply to be a mentor if you:",
"link": {
"label": "Join as a mentor",
"uri": "/mentorship/mentor-registration"
},
"items": [
"Want to extend your professional network",
"Want to contribute to the community",
"You are ready to share expertise",
"You want to get a new perspective and learn from your mentees"
],
"link": {
"label": "Join as a mentor",
"uri": "/mentorship/mentor-registration"
}
]
},
"menteeSection": {
"title": "Become a Mentee",
"description": "You should apply to be a mentee if you:",
"link": {
"label": "Check our mentors",
"uri": "/mentorship/mentors"
},
"items": [
"Want to start career in software engineering",
"Want to find a better job",
"Want to be promoted at work",
"Want to apply for a leadership position",
"Need support in advancing your career"
],
"link": {
"label": "Check our mentors",
"uri": "/mentorship/mentors"
}
]
},
"feedbackSection": {
"title": "What do participants think about our Mentorship Programme?",
"feedbacks": [
{
"name": "Lucy",
"feedback": "It is great to be able to share my experience as a newbie in Tech with someone that has more years and experience in the industry. It has definitely made me feel more comfortable with been a completely beginner again and confident that, if a put the hours in, one day it will be pay off.",
"memberType": "Mentor",
"year": 2024
"name": "Busra",
"feedback": "My session with my mentor exceeded my expectations. She's calm, caring, and incredibly knowledgeable. She offered valuable guidance not only on my career but also on life in general. My mentor truly embodies a growth mindset. I highly recommend her to any mentee seeking direction delivered with both compassion and wisdom.",
"memberType": "Mentee",
"year": 2025
},
{
"name": "Ana Smith",
"feedback": "I am exciting with this mentorship program. This is really help me to pursue my goals. I got encouragement, insights, knowledge from my mentor that makes me back on the track and focus to what I have now, upgrade it, then reach out my goals.",
"name": "Zayna",
"feedback": "My mentor really took the time to comprehend my mental barriers and gave personalised structure advice to move past them. I appreciated the mentors vulnerability in explaining similar scenarios they had been in to give me a sense of guidance and belonging. Their kindness and warmth during the session has really helped me to progress in my career due to feeling safe to open up to them.",
"memberType": "Mentee",
"year": 2024
"year": 2025
},
{
"name": "Jane",
"feedback": "My mentor has done an excellent job accommodating me and guiding me this year. This was my first experience as a mentee and it has taken time getting my footing. I entered this program because I felt isolated since I seemed to not have any clarity of my career and had a sense of general \"directionless\". Mentor has made me feel validated in my feeling and has helped me understand that my anxiety stems from not having a good understanding of the industry I hope to be a part of. Through our sessions and an exercise she gave me, I was gained a better perceptive on how exactly to direct my focus.",
"memberType": "Mentor",
"year": 2024
"name": "Anonymous",
"feedback": "Just the simple act of presenting my profile and explaining my difficulties to a professional helped me structure the ideas and information in my head, and that alone was a big help. My mentor was caring, involved, and encouraging.",
"memberType": "Mentee",
"year": 2025
},
{
"name": "Anonymous",
"feedback": "It is an extremely helpful program and the mentor is quite open with matching my needs. She also actively encourages me with when and how to make job applications. She has helped with reviewing my CV and Linkedin profile. We have started work on my own personal project and collaboratively code on it, with the mentor asking questions to reinforce learning.",
"name": "Hanna",
"feedback": "I am delighted to have had the opportunity to work with my mentor. Her explanations are always clear and thorough, and her enthusiasm and passion for our profession are truly contagious.\n Working with her has not only been educational but also incredibly motivating. Her guidance and support have given me a significant boost in my professional development. I am sincerely grateful to her for the valuable insights and encouragement she has provided.\nThank you!",
"memberType": "Mentee",
"year": 2024
}
]
}
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,26 @@
package com.wcc.platform.configuration;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.CONFLICT;
import static org.springframework.http.HttpStatus.FORBIDDEN;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import com.wcc.platform.domain.exceptions.DuplicatedMemberException;
import com.wcc.platform.domain.exceptions.ErrorDetails;
import com.wcc.platform.domain.exceptions.ForbiddenException;
import com.wcc.platform.domain.platform.mentorship.TechnicalAreaProficiency;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.mock.http.MockHttpInputMessage;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
Expand Down Expand Up @@ -93,4 +99,65 @@ void shouldReturnConflictForDataAccessException() {
assertEquals(CONFLICT, response.getStatusCode());
assertEquals(expectation, response.getBody());
}

@Test
@DisplayName(
"Given malformed JSON with unknown field, "
+ "when handling, then return BAD_REQUEST with field context")
void shouldReturnBadRequestForHttpMessageNotReadableException() {
var invalidJson =
"""
{
"name": "BACKEND",
"proficiencyLevel": "BEGINNER"
}
""";
var cause =
assertThrows(
com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException.class,
Comment thread
dricazenck marked this conversation as resolved.
() -> new ObjectMapper().readValue(invalidJson, TechnicalAreaProficiency.class));
var exception =
new HttpMessageNotReadableException(
"JSON parse error", cause, new MockHttpInputMessage(invalidJson.getBytes()));

var response =
globalExceptionHandler.handleHttpMessageNotReadableException(exception, webRequest);

var expectation =
new ErrorDetails(
BAD_REQUEST.value(),
"Unrecognized field 'name' at 'name'. "
+ "Allowed fields: proficiencyLevel, technicalArea",
DETAILS);
assertEquals(BAD_REQUEST, response.getStatusCode());
assertEquals(expectation, response.getBody());
}

@Test
@DisplayName(
"Given malformed JSON with invalid enum value, "
+ "when handling, then return BAD_REQUEST with fallback message")
void shouldReturnBadRequestWithFallbackMessageForInvalidEnumValue() {
var invalidJson =
"""
{
"technicalArea": "NOT_A_REAL_AREA",
"proficiencyLevel": "BEGINNER"
}
""";
var cause =
assertThrows(
InvalidFormatException.class,
() -> new ObjectMapper().readValue(invalidJson, TechnicalAreaProficiency.class));
var exception =
new HttpMessageNotReadableException(
"JSON parse error", cause, new MockHttpInputMessage(invalidJson.getBytes()));

var response =
globalExceptionHandler.handleHttpMessageNotReadableException(exception, webRequest);

var expectation = new ErrorDetails(BAD_REQUEST.value(), cause.getMessage(), DETAILS);
assertEquals(BAD_REQUEST, response.getStatusCode());
assertEquals(expectation, response.getBody());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,65 @@ void shouldCreateMentorAndReturnCreated() throws Exception {
.andExpect(jsonPath("$.profileStatus", is("PENDING")));
}

@Test
@DisplayName(
"Given mentor payload with unknown nested field, "
+ "when creating mentor, then return 400 with field context")
void shouldReturnBadRequestWithFieldContextForUnknownJsonField() throws Exception {
var invalidMentorPayload =
"""
{
"fullName": "fullName MENTOR",
"position": "position MENTOR",
"email": "email@mentor",
"slackDisplayName": "slackDisplayName",
"country": {
"countryCode": "ES",
"countryName": "Spain"
},
"city": "City",
"companyName": "Company name",
"images": [],
"network": [],
"bio": "Mentor bio",
"spokenLanguages": ["English"],
"skills": {
"yearsExperience": 2,
"areas": [
{
"name": "BACKEND",
"proficiencyLevel": "BEGINNER"
}
],
"languages": [],
"mentorshipFocus": []
},
"menteeSection": {
"idealMentee": "ideal mentee description",
"additional": "additional",
"longTerm": {
"maxMentees": 1,
"hoursPerMentee": 4
},
"adHoc": []
}
}
""";

mockMvc
.perform(
MockMvcRequestBuilders.post(API_MENTORS)
.header(API_KEY_HEADER, API_KEY_VALUE)
.contentType(APPLICATION_JSON)
.content(invalidMentorPayload))
.andExpect(status().isBadRequest())
.andExpect(
jsonPath(
"$.message",
is(
"Unrecognized field 'name' at 'skills.areas[0].name'. Allowed fields: proficiencyLevel, technicalArea")));
}

@Test
@DisplayName("Given valid mentor ID and DTO, when updating mentor, then return 200 OK")
void shouldUpdateMentorAndReturnOk() throws Exception {
Expand Down
Loading