diff --git a/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java b/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java index c1bec9cde..6a483b76e 100644 --- a/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java +++ b/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java @@ -12,6 +12,7 @@ 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.FeedbackNotFoundException; import com.wcc.platform.domain.exceptions.ForbiddenException; import com.wcc.platform.domain.exceptions.InvalidProgramTypeException; import com.wcc.platform.domain.exceptions.InvalidTokenException; @@ -50,7 +51,8 @@ public class GlobalExceptionHandler { NoSuchElementException.class, MemberNotFoundException.class, MentorNotFoundException.class, - ApplicationNotFoundException.class + ApplicationNotFoundException.class, + FeedbackNotFoundException.class }) @ResponseStatus(NOT_FOUND) public ResponseEntity handleNotFoundException( diff --git a/src/main/java/com/wcc/platform/controller/FeedbackController.java b/src/main/java/com/wcc/platform/controller/FeedbackController.java new file mode 100644 index 000000000..35ad7db02 --- /dev/null +++ b/src/main/java/com/wcc/platform/controller/FeedbackController.java @@ -0,0 +1,179 @@ +package com.wcc.platform.controller; + +import com.wcc.platform.domain.platform.feedback.Feedback; +import com.wcc.platform.domain.platform.feedback.FeedbackDto; +import com.wcc.platform.domain.platform.feedback.FeedbackSearchCriteria; +import com.wcc.platform.domain.platform.type.FeedbackType; +import com.wcc.platform.service.FeedbackService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import java.util.List; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +/** Rest controller for feedback APIs. */ +@RestController +@RequestMapping("/api/platform/v1/feedback") +@SecurityRequirement(name = "apiKey") +@Tag(name = "Feedback", description = "Feedback management APIs") +@AllArgsConstructor +@Validated +public class FeedbackController { + + private final FeedbackService feedbackService; + + /** + * API to retrieve all feedback with optional filters. + * + * @param reviewerId Filter by reviewer ID + * @param revieweeId Filter by reviewee I) + * @param mentorshipCycleId Filter by mentorship cycle ID + * @param feedbackType Filter by feedback type + * @param year Filter by year + * @param isAnonymous Filter by anonymous status + * @param isApproved Filter by approval status + * @return List of feedback matching the criteria + */ + @GetMapping + @Operation(summary = "Get all feedback with optional filters") + @ResponseStatus(HttpStatus.OK) + public ResponseEntity> getAllFeedback( + @Parameter(description = "Reviewer ID") @RequestParam(required = false) final Long reviewerId, + @Parameter(description = "Reviewee ID") @RequestParam(required = false) final Long revieweeId, + @Parameter(description = "Mentorship Cycle ID") @RequestParam(required = false) + final Long mentorshipCycleId, + @Parameter(description = "Feedback Type") @RequestParam(required = false) + final FeedbackType feedbackType, + @Parameter(description = "Year") @RequestParam(required = false) @Min(2000) @Max(2100) + final Integer year, + @Parameter(description = "Anonymous status") @RequestParam(required = false) + final Boolean isAnonymous, + @Parameter(description = "Approved status") @RequestParam(required = false) + final Boolean isApproved) { + final FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder() + .reviewerId(reviewerId) + .revieweeId(revieweeId) + .mentorshipCycleId(mentorshipCycleId) + .feedbackType(feedbackType) + .year(year) + .isAnonymous(isAnonymous) + .isApproved(isApproved) + .build(); + + final List feedback = feedbackService.getAllFeedback(criteria); + return ResponseEntity.ok(feedback); + } + + /** + * API to retrieve feedback by ID. + * + * @param feedbackId ID of the feedback + * @return Feedback details + */ + @GetMapping("/{feedbackId}") + @Operation(summary = "Get feedback by ID") + @ResponseStatus(HttpStatus.OK) + public ResponseEntity getFeedbackById( + @Parameter(description = "ID of the feedback") @PathVariable final Long feedbackId) { + final Feedback feedback = feedbackService.getFeedbackById(feedbackId); + return ResponseEntity.ok(feedback); + } + + /** + * API to create feedback. + * + * @param feedbackDto DTO containing feedback data + * @return Created feedback + */ + @PostMapping + @Operation(summary = "Create feedback") + @ResponseStatus(HttpStatus.CREATED) + public ResponseEntity createFeedback( + @Valid @RequestBody final FeedbackDto feedbackDto) { + final Feedback feedback = feedbackService.createFeedback(feedbackDto); + return new ResponseEntity<>(feedback, HttpStatus.CREATED); + } + + /** + * API to update feedback. + * + * @param feedbackId ID of the feedback to update + * @param feedbackDto DTO with updated feedback data + * @return Updated feedback + */ + @PutMapping("/{feedbackId}") + @Operation(summary = "Update feedback") + @ResponseStatus(HttpStatus.OK) + public ResponseEntity updateFeedback( + @Parameter(description = "ID of the feedback") @PathVariable final Long feedbackId, + @Valid @RequestBody final FeedbackDto feedbackDto) { + final Feedback feedback = feedbackService.updateFeedback(feedbackId, feedbackDto); + return ResponseEntity.ok(feedback); + } + + /** + * API to approve feedback (admin only). + * + * @param feedbackId ID of the feedback to approve + * @return No content + */ + @PatchMapping("/{feedbackId}/approve") + @Operation(summary = "Approve feedback (admin only)") + @ResponseStatus(HttpStatus.OK) + public ResponseEntity approveFeedback( + @Parameter(description = "ID of the feedback") @PathVariable final Long feedbackId) { + feedbackService.approveFeedback(feedbackId); + return ResponseEntity.ok().build(); + } + + /** + * API to set feedback anonymous status (to hide/show reviewer name). + * + * @param feedbackId ID of the feedback + * @param isAnonymous true to hide reviewer name, false to show reviewer name + * @return No content + */ + @PatchMapping("/{feedbackId}/anonymous-status") + @Operation(summary = "Update feedback anonymous status (to hide/show reviewer name)") + @ResponseStatus(HttpStatus.OK) + public ResponseEntity updateFeedbackAnonymousStatus( + @Parameter(description = "ID of the feedback") @PathVariable final Long feedbackId, + @Parameter(description = "Is anonymous") @RequestParam final Boolean isAnonymous) { + feedbackService.updateFeedbackAnonymousStatus(feedbackId, isAnonymous); + return ResponseEntity.ok().build(); + } + + /** + * API to delete feedback. + * + * @param feedbackId ID of the feedback to delete + * @return No content + */ + @DeleteMapping("/{feedbackId}") + @Operation(summary = "Delete feedback") + @ResponseStatus(HttpStatus.NO_CONTENT) + public ResponseEntity deleteFeedback( + @Parameter(description = "ID of the feedback") @PathVariable final Long feedbackId) { + feedbackService.deleteFeedback(feedbackId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/com/wcc/platform/domain/exceptions/FeedbackNotFoundException.java b/src/main/java/com/wcc/platform/domain/exceptions/FeedbackNotFoundException.java new file mode 100644 index 000000000..0f99a3634 --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/exceptions/FeedbackNotFoundException.java @@ -0,0 +1,16 @@ +package com.wcc.platform.domain.exceptions; + +import lombok.extern.slf4j.Slf4j; + +/** Platform Feedback not found exception. */ +@Slf4j +public class FeedbackNotFoundException extends RuntimeException { + + public FeedbackNotFoundException(final Long feedbackId) { + super("Feedback with id: " + feedbackId + " not found."); + } + + public FeedbackNotFoundException(final String message) { + super("Feedback not found: " + message); + } +} diff --git a/src/main/java/com/wcc/platform/domain/platform/feedback/Feedback.java b/src/main/java/com/wcc/platform/domain/platform/feedback/Feedback.java new file mode 100644 index 000000000..b5d88373d --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/platform/feedback/Feedback.java @@ -0,0 +1,48 @@ +package com.wcc.platform.domain.platform.feedback; + +import com.wcc.platform.domain.platform.type.FeedbackType; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import java.time.OffsetDateTime; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; + +/** Feedback for tracking member feedback. */ +@NoArgsConstructor +@AllArgsConstructor +@ToString +@EqualsAndHashCode +@Getter +@Builder(toBuilder = true) +public class Feedback { + + @Setter private Long id; + @NotNull private Long reviewerId; + private String reviewerName; + private Long revieweeId; // For MENTOR_REVIEW + private String revieweeName; + private Long mentorshipCycleId; // For MENTORSHIP_PROGRAM + @NotNull private FeedbackType feedbackType; + + @Min(1) + @Max(5) + private Integer rating; + + @NotBlank private String feedbackText; + + @Min(2000) + @Max(2100) + private Integer year; + + @Setter private Boolean isAnonymous; + @Setter private Boolean isApproved; + private OffsetDateTime createdAt; + private OffsetDateTime updatedAt; +} diff --git a/src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackDto.java b/src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackDto.java new file mode 100644 index 000000000..04c9f9c06 --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackDto.java @@ -0,0 +1,58 @@ +package com.wcc.platform.domain.platform.feedback; + +import com.wcc.platform.domain.platform.feedback.validation.ValidFeedback; +import com.wcc.platform.domain.platform.type.FeedbackType; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +/** DTO for feedback creation/update - uses IDs for member identification. */ +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Builder +@ValidFeedback +public class FeedbackDto { + private Long id; + @NotNull private Long reviewerId; + private Long revieweeId; + private Long mentorshipCycleId; + @NotNull private FeedbackType feedbackType; + + @Min(1) + @Max(5) + private Integer rating; + + @NotBlank private String feedbackText; + + @Min(2000) + @Max(2100) + private Integer year; + + @NotNull private Boolean isAnonymous; + + /** + * Convert to domain object. + * + * @return Feedback domain entity + */ + public Feedback merge() { + return Feedback.builder() + .id(id) + .reviewerId(reviewerId) + .revieweeId(revieweeId) + .mentorshipCycleId(mentorshipCycleId) + .feedbackType(feedbackType) + .rating(rating) + .feedbackText(feedbackText) + .year(year) + .isAnonymous(isAnonymous) + .isApproved(false) // Default - requires admin approval + .build(); + } +} diff --git a/src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackSearchCriteria.java b/src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackSearchCriteria.java new file mode 100644 index 000000000..fa774debe --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackSearchCriteria.java @@ -0,0 +1,25 @@ +package com.wcc.platform.domain.platform.feedback; + +import com.wcc.platform.domain.platform.type.FeedbackType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@NoArgsConstructor +@AllArgsConstructor +@ToString +@EqualsAndHashCode +@Getter +@Builder +public class FeedbackSearchCriteria { + private Long reviewerId; + private Long revieweeId; + private Long mentorshipCycleId; + private FeedbackType feedbackType; + private Integer year; + private Boolean isAnonymous; + private Boolean isApproved; +} diff --git a/src/main/java/com/wcc/platform/domain/platform/feedback/validation/FeedbackValidator.java b/src/main/java/com/wcc/platform/domain/platform/feedback/validation/FeedbackValidator.java new file mode 100644 index 000000000..5988d9559 --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/platform/feedback/validation/FeedbackValidator.java @@ -0,0 +1,67 @@ +package com.wcc.platform.domain.platform.feedback.validation; + +import com.wcc.platform.domain.platform.feedback.FeedbackDto; +import com.wcc.platform.domain.platform.type.FeedbackType; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +/** Validator for FeedbackDto that checks conditional field requirements. */ +public class FeedbackValidator implements ConstraintValidator { + + @Override + public boolean isValid(final FeedbackDto dto, final ConstraintValidatorContext context) { + if (dto == null) { + return true; // Let @NotNull handle null checks + } + + context.disableDefaultConstraintViolation(); + + return validateMentorshipCycle(dto, context) + && validateRevieweeId(dto, context) + && validateRating(dto, context); + } + + private boolean validateMentorshipCycle( + final FeedbackDto dto, final ConstraintValidatorContext context) { + // Validate mentorshipCycleId for MENTORSHIP_PROGRAM and MENTOR_REVIEW + if ((dto.getFeedbackType() == FeedbackType.MENTORSHIP_PROGRAM + || dto.getFeedbackType() == FeedbackType.MENTOR_REVIEW) + && dto.getMentorshipCycleId() == null) { + context + .buildConstraintViolationWithTemplate( + "mentorshipCycleId is required for " + dto.getFeedbackType() + " feedback") + .addPropertyNode("mentorshipCycleId") + .addConstraintViolation(); + return false; + } + return true; + } + + private boolean validateRevieweeId( + final FeedbackDto dto, final ConstraintValidatorContext context) { + // Validate revieweeId for MENTOR_REVIEW + if (dto.getFeedbackType() == FeedbackType.MENTOR_REVIEW && dto.getRevieweeId() == null) { + context + .buildConstraintViolationWithTemplate("revieweeId is required for MENTOR_REVIEW feedback") + .addPropertyNode("revieweeId") + .addConstraintViolation(); + return false; + } + return true; + } + + private boolean validateRating(final FeedbackDto dto, final ConstraintValidatorContext context) { + // Validate rating for MENTOR_REVIEW and MENTORSHIP_PROGRAM + if ((dto.getFeedbackType() == FeedbackType.MENTOR_REVIEW + || dto.getFeedbackType() == FeedbackType.MENTORSHIP_PROGRAM) + && dto.getRating() == null) { + context + .buildConstraintViolationWithTemplate( + "rating is required for " + dto.getFeedbackType() + " feedback") + .addPropertyNode("rating") + .addConstraintViolation(); + return false; + } + return true; + } +} diff --git a/src/main/java/com/wcc/platform/domain/platform/feedback/validation/ValidFeedback.java b/src/main/java/com/wcc/platform/domain/platform/feedback/validation/ValidFeedback.java new file mode 100644 index 000000000..e404c1498 --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/platform/feedback/validation/ValidFeedback.java @@ -0,0 +1,20 @@ +package com.wcc.platform.domain.platform.feedback.validation; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** Custom validation annotation for FeedbackDto. */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = FeedbackValidator.class) +public @interface ValidFeedback { + String message() default "Invalid feedback data"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/src/main/java/com/wcc/platform/domain/platform/type/FeedbackType.java b/src/main/java/com/wcc/platform/domain/platform/type/FeedbackType.java new file mode 100644 index 000000000..323dab3bc --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/platform/type/FeedbackType.java @@ -0,0 +1,33 @@ +package com.wcc.platform.domain.platform.type; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** Types of feedback. */ +@Getter +@AllArgsConstructor +public enum FeedbackType { + MENTOR_REVIEW(1, "Review of a mentor by a mentee"), + COMMUNITY_GENERAL(2, "General feedback about the community"), + MENTORSHIP_PROGRAM(3, "Feedback about the mentorship program"); + + private final int typeId; + private final String description; + + /** + * Retrieves the corresponding {@code FeedbackType} enum value based on a given type ID. If no + * match is found, the default {@code COMMUNITY_GENERAL} type is returned. + * + * @param typeId the integer ID representing a specific {@code FeedbackType} + * @return the {@code FeedbackType} that matches the given ID, or {@code COMMUNITY_GENERAL} if no + * match is found + */ + public static FeedbackType fromId(final int typeId) { + for (final FeedbackType type : values()) { + if (type.getTypeId() == typeId) { + return type; + } + } + throw new IllegalArgumentException("Unknown FeedbackType id: " + typeId); + } +} diff --git a/src/main/java/com/wcc/platform/repository/FeedbackRepository.java b/src/main/java/com/wcc/platform/repository/FeedbackRepository.java new file mode 100644 index 000000000..4d9c2e2cc --- /dev/null +++ b/src/main/java/com/wcc/platform/repository/FeedbackRepository.java @@ -0,0 +1,35 @@ +package com.wcc.platform.repository; + +import com.wcc.platform.domain.platform.feedback.Feedback; +import com.wcc.platform.domain.platform.feedback.FeedbackSearchCriteria; +import java.util.List; + +/** + * Repository interface for managing feedback entities. Provides methods to perform CRUD operations + * and additional feedback-related queries on the data source. + */ +public interface FeedbackRepository extends CrudRepository { + + /** + * Retrieve all feedbacks matching the specified search criteria. + * + * @param criteria the search criteria to filter feedbacks + * @return List of feedbacks + */ + List getAll(FeedbackSearchCriteria criteria); + + /** + * Approve feedback by ID. + * + * @param feedbackId the ID of the feedback to approve + */ + void approveFeedback(Long feedbackId); + + /** + * Set anonymous status of feedback. + * + * @param feedbackId the ID of the feedback + * @param isAnonymous true to hide reviewer name, false to show reviewer name + */ + void updateAnonymousStatus(Long feedbackId, Boolean isAnonymous); +} diff --git a/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java b/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java new file mode 100644 index 000000000..5f2958660 --- /dev/null +++ b/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java @@ -0,0 +1,123 @@ +package com.wcc.platform.repository.postgres; + +import com.wcc.platform.domain.platform.feedback.Feedback; +import com.wcc.platform.domain.platform.feedback.FeedbackSearchCriteria; +import com.wcc.platform.repository.FeedbackRepository; +import com.wcc.platform.repository.postgres.component.FeedbackMapper; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +/** + * Implementation of the FeedbackRepository interface for managing Feedback entities using + * PostgreSQL as the data source. This class interacts with the database using SQL queries and maps + * the result sets to Feedback objects with the help of FeedbackRowMapper. + */ +@Repository +@RequiredArgsConstructor +public class PostgresFeedbackRepository implements FeedbackRepository { + + private static final String DELETE_SQL = "DELETE FROM feedback WHERE id = ?"; + private static final String SELECT_BY_ID = + "SELECT f.*, m1.full_name AS reviewer_name, m2.full_name AS reviewee_name FROM feedback f " + + "LEFT JOIN members m1 ON m1.id = f.reviewer_id " + + "LEFT JOIN members m2 ON m2.id = f.reviewee_id " + + "WHERE f.id = ?"; + private static final String APPROVE_FEEDBACK = + "UPDATE feedback SET is_approved = true WHERE id = ?"; + private static final String SET_ANONYMOUS_STATUS = + "UPDATE feedback SET is_anonymous = ? WHERE id = ?"; + + private final JdbcTemplate jdbc; + private final FeedbackMapper feedbackMapper; + + @Override + public Feedback create(final Feedback entity) { + final Long feedbackId = feedbackMapper.addFeedback(entity); + return findById(feedbackId).orElseThrow(); + } + + @Override + public Feedback update(final Long id, final Feedback entity) { + feedbackMapper.updateFeedback(entity, id); + return findById(id).orElseThrow(); + } + + @Override + public Optional findById(final Long id) { + return jdbc.query( + SELECT_BY_ID, + rs -> { + if (rs.next()) { + return Optional.of(feedbackMapper.mapRowToFeedback(rs)); + } + return Optional.empty(); + }, + id); + } + + @Override + public void deleteById(final Long feedbackId) { + jdbc.update(DELETE_SQL, feedbackId); + } + + @Override + @SuppressWarnings({"PMD.InsufficientStringBufferDeclaration", "PMD.CognitiveComplexity"}) + public List getAll(final FeedbackSearchCriteria criteria) { + final StringBuilder sql = + new StringBuilder( + "SELECT f.*, m1.full_name AS reviewer_name, m2.full_name AS reviewee_name " + + "FROM feedback f " + + "LEFT JOIN members m1 ON m1.id = f.reviewer_id " + + "LEFT JOIN members m2 ON m2.id = f.reviewee_id " + + "WHERE 1 = 1"); + final List params = new ArrayList<>(); + + if (criteria != null) { + if (criteria.getReviewerId() != null) { + sql.append(" AND reviewer_id = ?"); + params.add(criteria.getReviewerId()); + } + if (criteria.getRevieweeId() != null) { + sql.append(" AND reviewee_id = ?"); + params.add(criteria.getRevieweeId()); + } + if (criteria.getFeedbackType() != null) { + sql.append(" AND feedback_type_id = ?"); + params.add(criteria.getFeedbackType().getTypeId()); + } + if (criteria.getYear() != null) { + sql.append(" AND feedback_year = ?"); + params.add(criteria.getYear()); + } + if (criteria.getMentorshipCycleId() != null) { + sql.append(" AND mentorship_cycle_id = ?"); + params.add(criteria.getMentorshipCycleId()); + } + if (criteria.getIsApproved() != null) { + sql.append(" AND is_approved = ?"); + params.add(criteria.getIsApproved()); + } + if (criteria.getIsAnonymous() != null) { + sql.append(" AND is_anonymous = ?"); + params.add(criteria.getIsAnonymous()); + } + } + + return jdbc.query( + sql.toString(), (rs, rowNum) -> feedbackMapper.mapRowToFeedback(rs), params.toArray()); + } + + @Override + public void approveFeedback(final Long feedbackId) { + jdbc.update(APPROVE_FEEDBACK, feedbackId); + } + + @Override + public void updateAnonymousStatus(final Long feedbackId, final Boolean isAnonymous) { + jdbc.update(SET_ANONYMOUS_STATUS, isAnonymous, feedbackId); + } +} diff --git a/src/main/java/com/wcc/platform/repository/postgres/component/FeedbackMapper.java b/src/main/java/com/wcc/platform/repository/postgres/component/FeedbackMapper.java new file mode 100644 index 000000000..6a10b22d8 --- /dev/null +++ b/src/main/java/com/wcc/platform/repository/postgres/component/FeedbackMapper.java @@ -0,0 +1,113 @@ +package com.wcc.platform.repository.postgres.component; + +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_CREATED_AT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_FB_TYPE_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_FEEDBACK_TEXT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_IS_ANONYMOUS; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_IS_APPROVED; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_MS_CYCLE_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_RATING; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_REVIEWEE_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_REVIEWEE_NAME; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_REVIEWER_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_REVIEWER_NAME; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_UPDATED_AT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_YEAR; + +import com.wcc.platform.domain.platform.feedback.Feedback; +import com.wcc.platform.domain.platform.type.FeedbackType; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.OffsetDateTime; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +/** Maps database result sets to Feedback domain objects. */ +@Component +@RequiredArgsConstructor +@SuppressWarnings("PMD.TooManyStaticImports") +public class FeedbackMapper { + /* default */ + static final String INSERT_SQL = + "INSERT INTO feedback (" + + "reviewer_id, reviewee_id, mentorship_cycle_id, feedback_type_id, " + + "rating, feedback_text, feedback_year, is_anonymous, is_approved) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id"; + + /* default */ + static final String UPDATE_SQL = + "UPDATE feedback SET " + + "reviewer_id = ?, reviewee_id = ?, mentorship_cycle_id = ?, " + + "feedback_type_id = ?, rating = ?, feedback_text = ?, " + + "feedback_year = ?, is_anonymous = ?, is_approved = ?, " + + "updated_at = CURRENT_TIMESTAMP " + + "WHERE id = ?"; + + private final JdbcTemplate jdbc; + + /** Maps a database row to a Feedback object. */ + public Feedback mapRowToFeedback(final ResultSet rs) throws SQLException { + return Feedback.builder() + .id(rs.getLong(COL_ID)) + .reviewerId(rs.getLong(COL_REVIEWER_ID)) + .reviewerName(rs.getString(COL_REVIEWER_NAME)) + .revieweeId(rs.getObject(COL_REVIEWEE_ID) != null ? rs.getLong(COL_REVIEWEE_ID) : null) + .revieweeName(rs.getString(COL_REVIEWEE_NAME)) + .mentorshipCycleId( + rs.getObject(COL_MS_CYCLE_ID) != null ? rs.getLong(COL_MS_CYCLE_ID) : null) + .feedbackType(FeedbackType.fromId(rs.getInt(COL_FB_TYPE_ID))) + .rating(rs.getObject(COL_RATING) != null ? rs.getInt(COL_RATING) : null) + .feedbackText(rs.getString(COL_FEEDBACK_TEXT)) + .year(rs.getObject(COL_YEAR) != null ? rs.getInt(COL_YEAR) : null) + .isAnonymous(rs.getBoolean(COL_IS_ANONYMOUS)) + .isApproved(rs.getBoolean(COL_IS_APPROVED)) + .createdAt( + rs.getObject(COL_CREATED_AT) != null + ? rs.getObject(COL_CREATED_AT, OffsetDateTime.class) + : null) + .updatedAt( + rs.getObject(COL_UPDATED_AT) != null + ? rs.getObject(COL_UPDATED_AT, OffsetDateTime.class) + : null) + .build(); + } + + /** Adds a new feedback to the database and returns the feedback ID. */ + public Long addFeedback(final Feedback feedback) { + return jdbc.queryForObject( + INSERT_SQL, + Long.class, + feedback.getReviewerId(), + feedback.getRevieweeId(), + feedback.getMentorshipCycleId(), + feedback.getFeedbackType().getTypeId(), + feedback.getRating(), + feedback.getFeedbackText(), + feedback.getYear(), + feedback.getIsAnonymous(), + feedback.getIsApproved()); + } + + /** + * Updates an existing feedback in the database. + * + * @param feedback the feedback entity with updated values + * @param feedbackId the ID of the feedback to update + */ + public void updateFeedback(final Feedback feedback, final Long feedbackId) { + jdbc.update( + UPDATE_SQL, + feedback.getReviewerId(), + feedback.getRevieweeId(), + feedback.getMentorshipCycleId(), + feedback.getFeedbackType().getTypeId(), + feedback.getRating(), + feedback.getFeedbackText(), + feedback.getYear(), + feedback.getIsAnonymous(), + feedback.getIsApproved(), + feedbackId); + } +} diff --git a/src/main/java/com/wcc/platform/repository/postgres/constants/FeedbackConstants.java b/src/main/java/com/wcc/platform/repository/postgres/constants/FeedbackConstants.java new file mode 100644 index 000000000..3bc4a9437 --- /dev/null +++ b/src/main/java/com/wcc/platform/repository/postgres/constants/FeedbackConstants.java @@ -0,0 +1,24 @@ +package com.wcc.platform.repository.postgres.constants; + +/** Constants related to Feedback entity. */ +@SuppressWarnings("PMD.DataClass") +public final class FeedbackConstants { + + public static final String TABLE = "feedback"; + public static final String COL_ID = "id"; + public static final String COL_REVIEWER_ID = "reviewer_id"; + public static final String COL_REVIEWEE_ID = "reviewee_id"; + public static final String COL_REVIEWER_NAME = "reviewer_name"; + public static final String COL_REVIEWEE_NAME = "reviewee_name"; + public static final String COL_MS_CYCLE_ID = "mentorship_cycle_id"; + public static final String COL_FB_TYPE_ID = "feedback_type_id"; + public static final String COL_RATING = "rating"; + public static final String COL_FEEDBACK_TEXT = "feedback_text"; + public static final String COL_YEAR = "feedback_year"; + public static final String COL_IS_ANONYMOUS = "is_anonymous"; + public static final String COL_IS_APPROVED = "is_approved"; + public static final String COL_CREATED_AT = "created_at"; + public static final String COL_UPDATED_AT = "updated_at"; + + private FeedbackConstants() {} +} diff --git a/src/main/java/com/wcc/platform/service/FeedbackService.java b/src/main/java/com/wcc/platform/service/FeedbackService.java new file mode 100644 index 000000000..176027f86 --- /dev/null +++ b/src/main/java/com/wcc/platform/service/FeedbackService.java @@ -0,0 +1,152 @@ +package com.wcc.platform.service; + +import com.wcc.platform.domain.exceptions.FeedbackNotFoundException; +import com.wcc.platform.domain.exceptions.MemberNotFoundException; +import com.wcc.platform.domain.platform.feedback.Feedback; +import com.wcc.platform.domain.platform.feedback.FeedbackDto; +import com.wcc.platform.domain.platform.feedback.FeedbackSearchCriteria; +import com.wcc.platform.repository.FeedbackRepository; +import com.wcc.platform.repository.MemberRepository; +import java.util.List; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +/** Feedback service. */ +@Slf4j +@Service +public class FeedbackService { + + private final FeedbackRepository feedbackRepository; + private final MemberRepository memberRepository; + + public FeedbackService( + final FeedbackRepository feedbackRepository, final MemberRepository memberRepository) { + this.feedbackRepository = feedbackRepository; + this.memberRepository = memberRepository; + } + + /** + * Create new feedback. Validates that reviewer/reviewee exists (if provided). + * + * @return created Feedback + */ + public Feedback createFeedback(final FeedbackDto feedbackDto) { + validateReviewerExists(feedbackDto.getReviewerId()); + final Feedback feedback = feedbackDto.merge(); + + if (feedback.getRevieweeId() != null) { + validateRevieweeExists(feedback.getRevieweeId()); + } + + log.info( + "Creating feedback of type {} from reviewer {} with text: {}", + feedback.getFeedbackType(), + feedback.getReviewerId(), + feedback.getFeedbackText()); + + return feedbackRepository.create(feedback); + } + + /** + * Update feedback. Only reviewer or admin can update. + * + * @return updated Feedback + */ + public Feedback updateFeedback(final Long feedbackId, final FeedbackDto feedbackDto) { + final Feedback existing = + feedbackRepository + .findById(feedbackId) + .orElseThrow(() -> new FeedbackNotFoundException(feedbackId)); + + validateReviewerExists(feedbackDto.getReviewerId()); + + final Feedback updatedFeedback = feedbackDto.merge(); + + updatedFeedback.setId(feedbackId); + updatedFeedback.setIsApproved(existing.getIsApproved()); + updatedFeedback.setIsAnonymous(existing.getIsAnonymous()); + + if (updatedFeedback.getRevieweeId() != null) { + validateRevieweeExists(updatedFeedback.getRevieweeId()); + } + + return feedbackRepository.update(feedbackId, updatedFeedback); + } + + /** + * Get feedback by ID. + * + * @return Feedback if found + */ + public Feedback getFeedbackById(final Long feedbackId) { + return feedbackRepository + .findById(feedbackId) + .orElseThrow(() -> new FeedbackNotFoundException(feedbackId)); + } + + /** Approve feedback (admin only). */ + public void approveFeedback(final Long feedbackId) { + feedbackRepository + .findById(feedbackId) + .orElseThrow(() -> new FeedbackNotFoundException(feedbackId)); + + feedbackRepository.approveFeedback(feedbackId); + } + + /** Update anonymous status of feedback (to hide/show reviewer name). */ + public void updateFeedbackAnonymousStatus(final Long feedbackId, final Boolean isAnonymous) { + feedbackRepository + .findById(feedbackId) + .orElseThrow(() -> new FeedbackNotFoundException(feedbackId)); + + feedbackRepository.updateAnonymousStatus(feedbackId, isAnonymous); + } + + /** Delete feedback by ID. */ + public void deleteFeedback(final Long feedbackId) { + feedbackRepository + .findById(feedbackId) + .orElseThrow(() -> new FeedbackNotFoundException(feedbackId)); + + feedbackRepository.deleteById(feedbackId); + } + + /** + * Get all feedbacks matching the specified search criteria. + * + * @param criteria the search criteria to filter feedbacks + * @return List of feedbacks + */ + public List getAllFeedback(final FeedbackSearchCriteria criteria) { + if (criteria != null && criteria.getReviewerId() != null) { + validateReviewerExists(criteria.getReviewerId()); + } + if (criteria != null && criteria.getRevieweeId() != null) { + validateRevieweeExists(criteria.getRevieweeId()); + } + + return feedbackRepository.getAll(criteria); + } + + /** Validate that reviewer with given ID exists. */ + private void validateReviewerExists(final Long reviewerId) { + memberRepository + .findById(reviewerId) + .orElseThrow( + () -> { + log.warn("Reviewer with ID {} not found", reviewerId); + return new MemberNotFoundException(reviewerId); + }); + } + + /** Validate that reviewee with given ID exists. */ + private void validateRevieweeExists(final Long revieweeId) { + memberRepository + .findById(revieweeId) + .orElseThrow( + () -> { + log.warn("Reviewee with ID {} not found", revieweeId); + return new MemberNotFoundException(revieweeId); + }); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 50af1119e..0c9942044 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -65,7 +65,7 @@ file: app: cors: - allowed-origins: http://localhost:3000,https://localhost:3000 + allowed-origins: http://localhost:3000,https://localhost:3000,http://localhost:3001,https://localhost:3001 email: team-signature: "WCC Mentorship Team" reset-password: diff --git a/src/main/resources/db/migration/V36__20260406__create_feedback_system.sql b/src/main/resources/db/migration/V36__20260406__create_feedback_system.sql new file mode 100644 index 000000000..7845e3522 --- /dev/null +++ b/src/main/resources/db/migration/V36__20260406__create_feedback_system.sql @@ -0,0 +1,49 @@ +-- Table for feedback types +CREATE TABLE IF NOT EXISTS feedback_types +( + id SERIAL PRIMARY KEY, + name VARCHAR(100) UNIQUE NOT NULL, + description TEXT +); + +-- Insert feedback types +INSERT INTO feedback_types (id, name, description) +VALUES (1, 'MENTOR_REVIEW', 'Review of a mentor by a mentee'), + (2, 'COMMUNITY_GENERAL', 'General feedback about the community'), + (3, 'MENTORSHIP_PROGRAM', 'Feedback about the mentorship program') +ON CONFLICT (id) DO NOTHING; + +-- Feedback table +CREATE TABLE IF NOT EXISTS feedback +( + id BIGSERIAL PRIMARY KEY, + reviewer_id INTEGER NOT NULL REFERENCES members (id) ON DELETE CASCADE, + reviewee_id INTEGER REFERENCES members (id) ON DELETE SET NULL, + mentorship_cycle_id INTEGER REFERENCES mentorship_cycles (cycle_id) ON DELETE SET NULL, + feedback_type_id INTEGER NOT NULL REFERENCES feedback_types (id), + rating INTEGER CHECK (rating >= 1 AND rating <= 5), + feedback_text TEXT NOT NULL, + feedback_year INTEGER, + is_anonymous BOOLEAN DEFAULT TRUE, + is_approved BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT feedback_mentor_review_constraint CHECK ( + (feedback_type_id = 1 AND reviewee_id IS NOT NULL) -- MENTOR_REVIEW + OR (feedback_type_id IN (2, 3)) -- COMMUNITY_GENERAL or MENTORSHIP_PROGRAM + ), + + CONSTRAINT feedback_mentorship_program_constraint CHECK ( + feedback_type_id != 3 OR mentorship_cycle_id IS NOT NULL + ) +); + +-- Performance indexes +CREATE INDEX IF NOT EXISTS idx_feedback_reviewer ON feedback (reviewer_id); +CREATE INDEX IF NOT EXISTS idx_feedback_reviewee ON feedback (reviewee_id); +CREATE INDEX IF NOT EXISTS idx_feedback_type ON feedback (feedback_type_id); +CREATE INDEX IF NOT EXISTS idx_feedback_anonymous_approved ON feedback (is_anonymous, is_approved); +CREATE INDEX IF NOT EXISTS idx_feedback_year ON feedback (feedback_year); +CREATE INDEX IF NOT EXISTS idx_feedback_cycle ON feedback (mentorship_cycle_id); + diff --git a/src/main/resources/init-data/feedback.json b/src/main/resources/init-data/feedback.json new file mode 100644 index 000000000..d9775dd8f --- /dev/null +++ b/src/main/resources/init-data/feedback.json @@ -0,0 +1,91 @@ +[ + { + "reviewerId": 1, + "revieweeId": 2, + "mentorshipCycleId": 1, + "feedbackType": "MENTOR_REVIEW", + "rating": 5, + "feedbackText": "Excellent mentor! Very patient and knowledgeable. Helped me understand complex Spring Boot concepts and provided great career advice. Would highly recommend!", + "year": 2026, + "isAnonymous": false + }, + { + "reviewerId": 2, + "revieweeId": 3, + "mentorshipCycleId": 1, + "feedbackType": "MENTOR_REVIEW", + "rating": 4, + "feedbackText": "Great mentoring experience. The mentor was very supportive and always available to answer questions. Learned a lot about frontend development best practices.", + "year": 2026, + "isAnonymous": true + }, + { + "reviewerId": 3, + "feedbackType": "COMMUNITY_GENERAL", + "feedbackText": "Love this community! Very welcoming and inclusive environment. The support from other members has been incredible. Thank you for creating such a safe space for women in tech!", + "year": 2026, + "isAnonymous": false + }, + { + "reviewerId": 4, + "feedbackType": "COMMUNITY_GENERAL", + "feedbackText": "Amazing community with lots of opportunities to learn and grow. The events are well-organized and the networking opportunities are fantastic.", + "year": 2026, + "isAnonymous": true + }, + { + "reviewerId": 1, + "mentorshipCycleId": 1, + "feedbackType": "MENTORSHIP_PROGRAM", + "rating": 5, + "feedbackText": "The mentorship program structure is excellent. The matching process was thoughtful and my mentor was a perfect fit. The resources provided were very helpful.", + "year": 2026, + "isAnonymous": false + }, + { + "reviewerId": 2, + "mentorshipCycleId": 1, + "feedbackType": "MENTORSHIP_PROGRAM", + "rating": 4, + "feedbackText": "Great program overall! Would love to see more structured check-ins and maybe some group mentorship sessions. But overall very satisfied with the experience.", + "year": 2026, + "isAnonymous": false + }, + { + "reviewerId": 5, + "revieweeId": 2, + "mentorshipCycleId": 1, + "feedbackType": "MENTOR_REVIEW", + "rating": 5, + "feedbackText": "Outstanding mentor! Goes above and beyond to help mentees succeed. Provided valuable code reviews and helped me prepare for technical interviews.", + "year": 2026, + "isAnonymous": false + }, + { + "reviewerId": 6, + "revieweeId": 3, + "mentorshipCycleId": 1, + "feedbackType": "MENTOR_REVIEW", + "rating": 5, + "feedbackText": "My mentor was incredibly helpful in navigating my career transition into tech. Provided practical advice and was always encouraging. Grateful for this opportunity!", + "year": 2026, + "isAnonymous": true + }, + { + "reviewerId": 3, + "mentorshipCycleId": 1, + "feedbackType": "MENTORSHIP_PROGRAM", + "rating": 5, + "feedbackText": "This program changed my career trajectory! The structure, support, and quality of mentors is exceptional. Highly recommend to anyone looking to grow in tech.", + "year": 2026, + "isAnonymous": false + }, + { + "reviewerId": 7, + "feedbackType": "COMMUNITY_GENERAL", + "feedbackText": "Such a supportive and empowering community! The resources, events, and people here have helped me grow both professionally and personally. Thank you WCC!", + "year": 2026, + "isAnonymous": false + } +] + diff --git a/src/main/resources/init-data/mentee.json b/src/main/resources/init-data/mentee.json new file mode 100644 index 000000000..7a0b455ce --- /dev/null +++ b/src/main/resources/init-data/mentee.json @@ -0,0 +1,164 @@ +[ + { + "mentee": { + "fullName": "Mentee1 Surname1", + "position": "Junior Software Developer", + "email": "mentee1@example.com", + "slackDisplayName": "@Mentee1", + "country": { + "countryCode": "SI", + "countryName": "Slovenia" + }, + "city": "Ljubljana", + "companyName": "Tech Startup Inc", + "memberTypes": [ + "MENTEE" + ], + "images": [ + { + "path": "https://drive.google.com/file/d/mentee1-image-id", + "alt": "Mentee1 profile picture", + "type": "desktop" + } + ], + "network": [ + { + "type": "linkedin", + "link": "https://www.linkedin.com/in/mentee1" + }, + { + "type": "github", + "link": "https://github.com/mentee1" + } + ], + "pronouns": "she/her", + "pronounCategory": "FEMININE", + "isWomen": true, + "profileStatus": "PENDING", + "skills": { + "yearsExperience": 2, + "areas": [ + { + "technicalArea": "FRONTEND", + "proficiencyLevel": "INTERMEDIATE" + }, + { + "technicalArea": "BACKEND", + "proficiencyLevel": "BEGINNER" + } + ], + "languages": [ + { + "language": "JAVASCRIPT", + "proficiencyLevel": "INTERMEDIATE" + }, + { + "language": "PYTHON", + "proficiencyLevel": "BEGINNER" + } + ], + "mentorshipFocus": [ + "Grow from beginner to mid-level", + "Career Advice" + ] + }, + "spokenLanguages": [ + "English", + "Slovenian" + ], + "bio": "I'm a passionate junior developer eager to grow my skills in full-stack development. Looking for guidance in advancing my career and improving my technical skills.", + "availableHsMonth": 8 + }, + "mentorshipType": "LONG_TERM", + "cycleYear": 2026, + "applications": [ + { + "mentorId": 3, + "whyMentor": "I'm interested in learning backend development and this mentor has extensive experience in Java and Spring Boot.", + "priorityOrder": 1, + "applicationMessage": "I would love to learn from your experience in building scalable backend systems." + } + ] + }, + { + "mentee": { + "fullName": "Nevena Verbic", + "position": "Software Developer", + "email": "nevena.verbic@example.com", + "slackDisplayName": "@NevenaVerbic", + "country": { + "countryCode": "SI", + "countryName": "Slovenia" + }, + "city": "Ljubljana", + "companyName": "Tech Startup Inc", + "memberTypes": [ + "MENTEE" + ], + "images": [ + { + "path": "https://drive.google.com/file/d/example-image-id", + "alt": "Nevena profile picture", + "type": "desktop" + } + ], + "network": [ + { + "type": "linkedin", + "link": "https://www.linkedin.com/in/neve" + }, + { + "type": "github", + "link": "https://github.com/neve" + } + ], + "pronouns": "she/her", + "pronounCategory": "FEMININE", + "isWomen": true, + "profileStatus": "PENDING", + "skills": { + "yearsExperience": 2, + "areas": [ + { + "technicalArea": "FRONTEND", + "proficiencyLevel": "INTERMEDIATE" + }, + { + "technicalArea": "BACKEND", + "proficiencyLevel": "BEGINNER" + } + ], + "languages": [ + { + "language": "JAVASCRIPT", + "proficiencyLevel": "INTERMEDIATE" + }, + { + "language": "PYTHON", + "proficiencyLevel": "BEGINNER" + } + ], + "mentorshipFocus": [ + "Grow from beginner to mid-level", + "Career Advice" + ] + }, + "spokenLanguages": [ + "English", + "Slovenian" + ], + "bio": "I'm a passionate developer eager to grow my skills in full-stack development.", + "availableHsMonth": 8 + }, + "mentorshipType": "LONG_TERM", + "cycleYear": 2026, + "applications": [ + { + "mentorId": 1, + "whyMentor": "Experienced in Java and Spring Boot, exactly what I want to learn", + "priorityOrder": 1, + "applicationMessage": "I would love to learn backend development from you!" + } + ] + } +] diff --git a/src/main/resources/init-data/mentor.json b/src/main/resources/init-data/mentor.json index 9ba9eca9b..5e17a3393 100644 --- a/src/main/resources/init-data/mentor.json +++ b/src/main/resources/init-data/mentor.json @@ -20,13 +20,16 @@ "link": "https://www.linkedin.com/in/dricazenck/" } ], + "pronouns": "she/her", + "pronounCategory": "FEMININE", + "isWomen": true, "profileStatus": "ACTIVE", "skills": { "yearsExperience": 15, "areas": [ { "technicalArea": "BACKEND", - "proficiencyLevel": "ADVANCED" + "proficiencyLevel": "EXPERT" }, { "technicalArea": "FULLSTACK", @@ -39,16 +42,16 @@ ], "languages": [ { - "language": "java", - "proficiencyLevel": "ADVANCED" + "language": "JAVA", + "proficiencyLevel": "EXPERT" }, { - "language": "javascript", + "language": "JAVASCRIPT", "proficiencyLevel": "ADVANCED" }, { - "language": "Kotlin", - "proficiencyLevel": "ADVANCED" + "language": "KOTLIN", + "proficiencyLevel": "INTERMEDIATE" } ], "mentorshipFocus": [ @@ -95,5 +98,8 @@ "hours": 2 } ] - } + }, + "calendlyLink": "https://calendly.com/adriana-zencke", + "acceptMale": true, + "acceptPromotion": true } diff --git a/src/test/java/com/wcc/platform/controller/FeedbackControllerTest.java b/src/test/java/com/wcc/platform/controller/FeedbackControllerTest.java new file mode 100644 index 000000000..d479eb4b9 --- /dev/null +++ b/src/test/java/com/wcc/platform/controller/FeedbackControllerTest.java @@ -0,0 +1,468 @@ +package com.wcc.platform.controller; + +import static com.wcc.platform.factories.MockMvcRequestFactory.getRequest; +import static com.wcc.platform.factories.MockMvcRequestFactory.postRequest; +import static com.wcc.platform.factories.SetupFeedbackFactories.createCommunityGeneralFeedbackDtoTest; +import static com.wcc.platform.factories.SetupFeedbackFactories.createCommunityGeneralFeedbackTest; +import static com.wcc.platform.factories.SetupFeedbackFactories.createMentorReviewFeedbackDtoTest; +import static com.wcc.platform.factories.SetupFeedbackFactories.createMentorReviewFeedbackTest; +import static com.wcc.platform.factories.SetupFeedbackFactories.createMentorshipProgramFeedbackTest; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wcc.platform.configuration.SecurityConfig; +import com.wcc.platform.configuration.TestConfig; +import com.wcc.platform.domain.exceptions.FeedbackNotFoundException; +import com.wcc.platform.domain.exceptions.PlatformInternalException; +import com.wcc.platform.domain.platform.feedback.Feedback; +import com.wcc.platform.domain.platform.feedback.FeedbackDto; +import com.wcc.platform.domain.platform.feedback.FeedbackSearchCriteria; +import com.wcc.platform.domain.platform.type.FeedbackType; +import com.wcc.platform.service.FeedbackService; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +/** Unit test for Feedback APIs. */ +@ActiveProfiles("test") +@Import({SecurityConfig.class, TestConfig.class}) +@WebMvcTest(FeedbackController.class) +class FeedbackControllerTest { + + private static final String API_FEEDBACK = "/api/platform/v1/feedback"; + private static final String API_KEY_HEADER = "X-API-KEY"; + private static final String API_KEY_VALUE = "test-api-key"; + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Autowired private MockMvc mockMvc; + @MockBean private FeedbackService feedbackService; + + @Test + @DisplayName( + "Given valid feedback ID, when getting feedback by ID, then returns OK with feedback") + void testGetFeedbackByIdReturnsOk() throws Exception { + Long feedbackId = 1L; + Feedback mockFeedback = createMentorReviewFeedbackTest(); + when(feedbackService.getFeedbackById(feedbackId)).thenReturn(mockFeedback); + + mockMvc + .perform(getRequest(API_FEEDBACK + "/" + feedbackId).contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.reviewerId", is(1))) + .andExpect(jsonPath("$.reviewerName", is("Mentee Reviewer"))) + .andExpect(jsonPath("$.feedbackType", is("MENTOR_REVIEW"))) + .andExpect(jsonPath("$.rating", is(5))); + } + + @Test + @DisplayName( + "Given non-existent feedback ID, when getting feedback by ID, then returns not found") + void testGetFeedbackByIdNotFound() throws Exception { + Long feedbackId = 999L; + when(feedbackService.getFeedbackById(feedbackId)) + .thenThrow(new FeedbackNotFoundException(feedbackId)); + + mockMvc + .perform(getRequest(API_FEEDBACK + "/" + feedbackId).contentType(APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName( + "Given valid mentor review feedback DTO, when creating feedback, then returns created with feedback") + void testCreateFeedbackReturnsCreated() throws Exception { + FeedbackDto feedbackDto = createMentorReviewFeedbackDtoTest(); + Feedback mockFeedback = createMentorReviewFeedbackTest(); + when(feedbackService.createFeedback(any(FeedbackDto.class))).thenReturn(mockFeedback); + + mockMvc + .perform(postRequest(API_FEEDBACK, feedbackDto)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.reviewerId", is(1))) + .andExpect(jsonPath("$.revieweeId", is(2))) + .andExpect(jsonPath("$.feedbackType", is("MENTOR_REVIEW"))) + .andExpect(jsonPath("$.rating", is(5))) + .andExpect(jsonPath("$.feedbackText", is("This is a test feedback"))); + } + + @Test + @DisplayName( + "Given valid community general feedback DTO, when creating feedback, then returns created with feedback") + void testCreateCommunityFeedbackReturnsCreated() throws Exception { + FeedbackDto feedbackDto = createCommunityGeneralFeedbackDtoTest(); + Feedback mockFeedback = createCommunityGeneralFeedbackTest(); + when(feedbackService.createFeedback(any(FeedbackDto.class))).thenReturn(mockFeedback); + + mockMvc + .perform(postRequest(API_FEEDBACK, feedbackDto)) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.id", is(2))) + .andExpect(jsonPath("$.feedbackType", is("COMMUNITY_GENERAL"))) + .andExpect(jsonPath("$.rating", is(4))) + .andExpect(jsonPath("$.feedbackText", is("Great community experience"))); + } + + @Test + @DisplayName( + "Given valid feedback ID and update DTO, when updating feedback, then returns OK with updated feedback") + void testUpdateFeedbackReturnsOk() throws Exception { + Long feedbackId = 1L; + FeedbackDto feedbackDto = createMentorReviewFeedbackDtoTest(); + Feedback updatedFeedback = + createMentorReviewFeedbackTest().toBuilder() + .feedbackText("Updated feedback text") + .rating(4) + .build(); + + when(feedbackService.updateFeedback(eq(feedbackId), any(FeedbackDto.class))) + .thenReturn(updatedFeedback); + + mockMvc + .perform( + MockMvcRequestBuilders.put(API_FEEDBACK + "/" + feedbackId) + .header(API_KEY_HEADER, API_KEY_VALUE) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(feedbackDto))) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id", is(1))) + .andExpect(jsonPath("$.feedbackText", is("Updated feedback text"))) + .andExpect(jsonPath("$.rating", is(4))); + } + + @Test + @DisplayName("Given non-existent feedback ID, when updating feedback, then returns not found") + void testUpdateNonExistentFeedbackThrowsException() throws Exception { + Long nonExistentFeedbackId = 999L; + FeedbackDto feedbackDto = createMentorReviewFeedbackDtoTest(); + + when(feedbackService.updateFeedback(eq(nonExistentFeedbackId), any(FeedbackDto.class))) + .thenThrow(new FeedbackNotFoundException(nonExistentFeedbackId)); + + mockMvc + .perform( + MockMvcRequestBuilders.put(API_FEEDBACK + "/" + nonExistentFeedbackId) + .header(API_KEY_HEADER, API_KEY_VALUE) + .contentType(APPLICATION_JSON) + .content(objectMapper.writeValueAsString(feedbackDto))) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Given valid feedback ID, when approving feedback, then returns OK") + void testApproveFeedbackReturnsOk() throws Exception { + Long feedbackId = 1L; + doNothing().when(feedbackService).approveFeedback(feedbackId); + + mockMvc + .perform( + MockMvcRequestBuilders.patch(API_FEEDBACK + "/" + feedbackId + "/approve") + .header(API_KEY_HEADER, API_KEY_VALUE)) + .andExpect(status().isOk()); + + verify(feedbackService).approveFeedback(feedbackId); + } + + @Test + @DisplayName("Given non-existent feedback ID, when approving feedback, then returns not found") + void testApproveNonExistentFeedbackThrowsException() throws Exception { + Long nonExistentFeedbackId = 999L; + doThrow(new FeedbackNotFoundException(nonExistentFeedbackId)) + .when(feedbackService) + .approveFeedback(nonExistentFeedbackId); + + mockMvc + .perform( + MockMvcRequestBuilders.patch(API_FEEDBACK + "/" + nonExistentFeedbackId + "/approve") + .header(API_KEY_HEADER, API_KEY_VALUE)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName( + "Given valid feedback ID and anonymous status true, when updating anonymous status, then returns OK") + void testUpdateFeedbackAnonymousStatusReturnsOk() throws Exception { + Long feedbackId = 1L; + Boolean isAnonymous = true; + doNothing().when(feedbackService).updateFeedbackAnonymousStatus(feedbackId, isAnonymous); + + mockMvc + .perform( + MockMvcRequestBuilders.patch(API_FEEDBACK + "/" + feedbackId + "/anonymous-status") + .header(API_KEY_HEADER, API_KEY_VALUE) + .param("isAnonymous", isAnonymous.toString())) + .andExpect(status().isOk()); + + verify(feedbackService).updateFeedbackAnonymousStatus(feedbackId, isAnonymous); + } + + @Test + @DisplayName( + "Given valid feedback ID and anonymous status false, when updating anonymous status, then returns OK") + void testUpdateFeedbackAnonymousStatusToFalseReturnsOk() throws Exception { + Long feedbackId = 1L; + Boolean isAnonymous = false; + doNothing().when(feedbackService).updateFeedbackAnonymousStatus(feedbackId, isAnonymous); + + mockMvc + .perform( + MockMvcRequestBuilders.patch(API_FEEDBACK + "/" + feedbackId + "/anonymous-status") + .header(API_KEY_HEADER, API_KEY_VALUE) + .param("isAnonymous", isAnonymous.toString())) + .andExpect(status().isOk()); + + verify(feedbackService).updateFeedbackAnonymousStatus(feedbackId, isAnonymous); + } + + @Test + @DisplayName("Given valid feedback ID, when deleting feedback, then returns no content") + void testDeleteFeedbackReturnsNoContent() throws Exception { + Long feedbackId = 1L; + doNothing().when(feedbackService).deleteFeedback(feedbackId); + + mockMvc + .perform( + MockMvcRequestBuilders.delete(API_FEEDBACK + "/" + feedbackId) + .header(API_KEY_HEADER, API_KEY_VALUE)) + .andExpect(status().isNoContent()); + + verify(feedbackService).deleteFeedback(feedbackId); + } + + @Test + @DisplayName("Given non-existent feedback ID, when deleting feedback, then returns not found") + void testDeleteNonExistentFeedbackThrowsException() throws Exception { + Long nonExistentFeedbackId = 999L; + doThrow(new FeedbackNotFoundException(nonExistentFeedbackId)) + .when(feedbackService) + .deleteFeedback(nonExistentFeedbackId); + + mockMvc + .perform( + MockMvcRequestBuilders.delete(API_FEEDBACK + "/" + nonExistentFeedbackId) + .header(API_KEY_HEADER, API_KEY_VALUE)) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Given no query filters, when getting all feedback, then returns all feedback") + void testGetAllFeedbackNoFilters() throws Exception { + Feedback feedback1 = createMentorReviewFeedbackTest(); + Feedback feedback2 = createCommunityGeneralFeedbackTest(); + Feedback feedback3 = createMentorshipProgramFeedbackTest(); + List mockFeedbackList = List.of(feedback1, feedback2, feedback3); + + when(feedbackService.getAllFeedback(any(FeedbackSearchCriteria.class))) + .thenReturn(mockFeedbackList); + + mockMvc + .perform(getRequest(API_FEEDBACK).contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(3))) + .andExpect(jsonPath("$[0].id", is(1))) + .andExpect(jsonPath("$[0].feedbackType", is("MENTOR_REVIEW"))) + .andExpect(jsonPath("$[1].id", is(2))) + .andExpect(jsonPath("$[1].feedbackType", is("COMMUNITY_GENERAL"))) + .andExpect(jsonPath("$[2].id", is(3))) + .andExpect(jsonPath("$[2].feedbackType", is("MENTORSHIP_PROGRAM"))); + + verify(feedbackService) + .getAllFeedback( + argThat( + criteria -> + criteria.getReviewerId() == null + && criteria.getRevieweeId() == null + && criteria.getFeedbackType() == null + && criteria.getYear() == null)); + } + + @Test + @DisplayName( + "Given reviewer ID filter, when getting all feedback, then returns feedback for reviewer") + void testGetAllFeedbackWithReviewerId() throws Exception { + Long reviewerId = 1L; + Feedback feedback1 = createMentorReviewFeedbackTest(); + Feedback feedback2 = createMentorReviewFeedbackTest(); + feedback2.setId(4L); + List mockFeedbackList = List.of(feedback1, feedback2); + + when(feedbackService.getAllFeedback(any(FeedbackSearchCriteria.class))) + .thenReturn(mockFeedbackList); + + mockMvc + .perform( + getRequest(API_FEEDBACK) + .param("reviewerId", reviewerId.toString()) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].reviewerId", is(1))) + .andExpect(jsonPath("$[1].reviewerId", is(1))); + + verify(feedbackService) + .getAllFeedback( + argThat( + criteria -> + criteria.getReviewerId().equals(reviewerId) + && criteria.getRevieweeId() == null + && criteria.getFeedbackType() == null)); + } + + @Test + @DisplayName( + "Given reviewee ID filter, when getting all feedback, then returns feedback for reviewee") + void testGetAllFeedbackWithRevieweeId() throws Exception { + Long revieweeId = 2L; + Feedback feedback1 = createMentorReviewFeedbackTest(); + List mockFeedbackList = List.of(feedback1); + + when(feedbackService.getAllFeedback(any(FeedbackSearchCriteria.class))) + .thenReturn(mockFeedbackList); + + mockMvc + .perform( + getRequest(API_FEEDBACK) + .param("revieweeId", revieweeId.toString()) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].revieweeId", is(2))); + + verify(feedbackService) + .getAllFeedback( + argThat( + criteria -> + criteria.getRevieweeId().equals(revieweeId) + && criteria.getReviewerId() == null + && criteria.getFeedbackType() == null)); + } + + @Test + @DisplayName( + "Given feedback type filter, when getting all feedback, then returns feedback of specified type") + void testGetAllFeedbackWithType() throws Exception { + Feedback feedback1 = createMentorReviewFeedbackTest(); + Feedback feedback2 = createMentorReviewFeedbackTest(); + feedback2.setId(5L); + List mockFeedbackList = List.of(feedback1, feedback2); + + when(feedbackService.getAllFeedback(any(FeedbackSearchCriteria.class))) + .thenReturn(mockFeedbackList); + + mockMvc + .perform( + getRequest(API_FEEDBACK) + .param("feedbackType", "MENTOR_REVIEW") + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].feedbackType", is("MENTOR_REVIEW"))) + .andExpect(jsonPath("$[1].feedbackType", is("MENTOR_REVIEW"))); + + verify(feedbackService) + .getAllFeedback( + argThat( + criteria -> + criteria.getFeedbackType() == FeedbackType.MENTOR_REVIEW + && criteria.getReviewerId() == null + && criteria.getRevieweeId() == null)); + } + + @Test + @DisplayName( + "Given year filter, when getting all feedback, then returns feedback for specified year") + void testGetAllFeedbackWithYear() throws Exception { + Integer year = 2026; + Feedback feedback1 = createMentorReviewFeedbackTest(); + Feedback feedback2 = createCommunityGeneralFeedbackTest(); + List mockFeedbackList = List.of(feedback1, feedback2); + + when(feedbackService.getAllFeedback(any(FeedbackSearchCriteria.class))) + .thenReturn(mockFeedbackList); + + mockMvc + .perform( + getRequest(API_FEEDBACK).param("year", year.toString()).contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(2))) + .andExpect(jsonPath("$[0].year", is(2026))) + .andExpect(jsonPath("$[1].year", is(2026))); + + verify(feedbackService) + .getAllFeedback( + argThat( + criteria -> + criteria.getYear().equals(year) + && criteria.getReviewerId() == null + && criteria.getRevieweeId() == null + && criteria.getFeedbackType() == null)); + } + + @Test + @DisplayName( + "Given multiple filters, when getting all feedback, then returns feedback matching all filters") + void testGetAllFeedbackMultipleFilters() throws Exception { + Long reviewerId = 1L; + Integer year = 2026; + Feedback feedback1 = createMentorReviewFeedbackTest(); + List mockFeedbackList = List.of(feedback1); + + when(feedbackService.getAllFeedback(any(FeedbackSearchCriteria.class))) + .thenReturn(mockFeedbackList); + + mockMvc + .perform( + getRequest(API_FEEDBACK) + .param("reviewerId", reviewerId.toString()) + .param("feedbackType", "MENTOR_REVIEW") + .param("year", year.toString()) + .contentType(APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$", hasSize(1))) + .andExpect(jsonPath("$[0].reviewerId", is(1))) + .andExpect(jsonPath("$[0].feedbackType", is("MENTOR_REVIEW"))) + .andExpect(jsonPath("$[0].year", is(2026))); + + verify(feedbackService) + .getAllFeedback( + argThat( + criteria -> + criteria.getReviewerId().equals(reviewerId) + && criteria.getFeedbackType() == FeedbackType.MENTOR_REVIEW + && criteria.getYear().equals(year))); + } + + @Test + @DisplayName( + "Given service throws exception, when getting all feedback, then returns internal server error") + void testInternalServerError() throws Exception { + when(feedbackService.getAllFeedback(any(FeedbackSearchCriteria.class))) + .thenThrow(new PlatformInternalException("Invalid Json", new RuntimeException())); + + mockMvc + .perform(getRequest(API_FEEDBACK).contentType(APPLICATION_JSON)) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.status", is(500))) + .andExpect(jsonPath("$.message", is("Invalid Json"))) + .andExpect(jsonPath("$.details", is("uri=/api/platform/v1/feedback"))); + } +} diff --git a/src/test/java/com/wcc/platform/domain/platform/feedback/FeedbackSearchCriteriaTest.java b/src/test/java/com/wcc/platform/domain/platform/feedback/FeedbackSearchCriteriaTest.java new file mode 100644 index 000000000..c8b7b4a97 --- /dev/null +++ b/src/test/java/com/wcc/platform/domain/platform/feedback/FeedbackSearchCriteriaTest.java @@ -0,0 +1,231 @@ +package com.wcc.platform.domain.platform.feedback; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.wcc.platform.domain.platform.type.FeedbackType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Unit tests for FeedbackSearchCriteria domain object. */ +class FeedbackSearchCriteriaTest { + + private FeedbackSearchCriteria criteria; + + @BeforeEach + void setUp() { + criteria = + FeedbackSearchCriteria.builder() + .reviewerId(1L) + .revieweeId(2L) + .mentorshipCycleId(3L) + .feedbackType(FeedbackType.MENTOR_REVIEW) + .year(2026) + .isAnonymous(false) + .isApproved(true) + .build(); + } + + @Test + @DisplayName("Given builder, when all fields set, then object created successfully") + void testBuilder() { + assertNotNull(criteria); + assertEquals(1L, criteria.getReviewerId()); + assertEquals(2L, criteria.getRevieweeId()); + assertEquals(3L, criteria.getMentorshipCycleId()); + assertEquals(FeedbackType.MENTOR_REVIEW, criteria.getFeedbackType()); + assertEquals(2026, criteria.getYear()); + assertEquals(false, criteria.getIsAnonymous()); + assertEquals(true, criteria.getIsApproved()); + } + + @Test + @DisplayName("Given same criteria, when equals called, then returns true") + void testEquals() { + FeedbackSearchCriteria sameCriteria = + FeedbackSearchCriteria.builder() + .reviewerId(1L) + .revieweeId(2L) + .mentorshipCycleId(3L) + .feedbackType(FeedbackType.MENTOR_REVIEW) + .year(2026) + .isAnonymous(false) + .isApproved(true) + .build(); + + assertEquals(criteria, sameCriteria); + } + + @Test + @DisplayName("Given different criteria, when equals called, then returns false") + void testNotEquals() { + FeedbackSearchCriteria differentCriteria = + FeedbackSearchCriteria.builder() + .reviewerId(99L) + .revieweeId(2L) + .mentorshipCycleId(3L) + .feedbackType(FeedbackType.MENTOR_REVIEW) + .year(2026) + .isAnonymous(false) + .isApproved(true) + .build(); + + assertNotEquals(criteria, differentCriteria); + } + + @Test + @DisplayName("Given same criteria, when hashCode called, then returns same hash") + void testHashCode() { + FeedbackSearchCriteria sameCriteria = + FeedbackSearchCriteria.builder() + .reviewerId(1L) + .revieweeId(2L) + .mentorshipCycleId(3L) + .feedbackType(FeedbackType.MENTOR_REVIEW) + .year(2026) + .isAnonymous(false) + .isApproved(true) + .build(); + + assertEquals(criteria.hashCode(), sameCriteria.hashCode()); + } + + @Test + @DisplayName("Given different criteria, when hashCode called, then returns different hash") + void testHashCodeNotEquals() { + FeedbackSearchCriteria differentCriteria = + FeedbackSearchCriteria.builder() + .reviewerId(1L) + .revieweeId(2L) + .mentorshipCycleId(3L) + .feedbackType(FeedbackType.COMMUNITY_GENERAL) + .year(2026) + .isAnonymous(false) + .isApproved(true) + .build(); + + assertNotEquals(criteria.hashCode(), differentCriteria.hashCode()); + } + + @Test + @DisplayName("Given criteria, when toString called, then contains field values") + void testToString() { + String toString = criteria.toString(); + + assertTrue(toString.contains("reviewerId=1")); + assertTrue(toString.contains("revieweeId=2")); + assertTrue(toString.contains("mentorshipCycleId=3")); + assertTrue(toString.contains("MENTOR_REVIEW")); + assertTrue(toString.contains("year=2026")); + } + + @Test + @DisplayName("Given no-arg constructor, when created, then all fields null") + void testNoArgsConstructor() { + FeedbackSearchCriteria emptyCriteria = new FeedbackSearchCriteria(); + + assertNull(emptyCriteria.getReviewerId()); + assertNull(emptyCriteria.getRevieweeId()); + assertNull(emptyCriteria.getMentorshipCycleId()); + assertNull(emptyCriteria.getFeedbackType()); + assertNull(emptyCriteria.getYear()); + assertNull(emptyCriteria.getIsAnonymous()); + assertNull(emptyCriteria.getIsApproved()); + } + + @Test + @DisplayName("Given all-arg constructor, when created, then all fields set") + void testAllArgsConstructor() { + FeedbackSearchCriteria allArgsCriteria = + new FeedbackSearchCriteria( + 10L, 20L, 30L, FeedbackType.MENTORSHIP_PROGRAM, 2027, true, false); + + assertEquals(10L, allArgsCriteria.getReviewerId()); + assertEquals(20L, allArgsCriteria.getRevieweeId()); + assertEquals(30L, allArgsCriteria.getMentorshipCycleId()); + assertEquals(FeedbackType.MENTORSHIP_PROGRAM, allArgsCriteria.getFeedbackType()); + assertEquals(2027, allArgsCriteria.getYear()); + assertEquals(true, allArgsCriteria.getIsAnonymous()); + assertEquals(false, allArgsCriteria.getIsApproved()); + } + + @Test + @DisplayName("Given partial criteria, when built, then only specified fields set") + void testPartialBuilder() { + FeedbackSearchCriteria partialCriteria = + FeedbackSearchCriteria.builder().reviewerId(5L).year(2025).build(); + + assertEquals(5L, partialCriteria.getReviewerId()); + assertEquals(2025, partialCriteria.getYear()); + assertNull(partialCriteria.getRevieweeId()); + assertNull(partialCriteria.getMentorshipCycleId()); + assertNull(partialCriteria.getFeedbackType()); + assertNull(partialCriteria.getIsAnonymous()); + assertNull(partialCriteria.getIsApproved()); + } + + @Test + @DisplayName("Given criteria with all types, when created, then covers all feedback types") + void testAllFeedbackTypes() { + FeedbackSearchCriteria mentorReviewCriteria = + FeedbackSearchCriteria.builder().feedbackType(FeedbackType.MENTOR_REVIEW).build(); + assertEquals(FeedbackType.MENTOR_REVIEW, mentorReviewCriteria.getFeedbackType()); + + FeedbackSearchCriteria mentorshipProgramCriteria = + FeedbackSearchCriteria.builder().feedbackType(FeedbackType.MENTORSHIP_PROGRAM).build(); + assertEquals(FeedbackType.MENTORSHIP_PROGRAM, mentorshipProgramCriteria.getFeedbackType()); + + FeedbackSearchCriteria communityGeneralCriteria = + FeedbackSearchCriteria.builder().feedbackType(FeedbackType.COMMUNITY_GENERAL).build(); + assertEquals(FeedbackType.COMMUNITY_GENERAL, communityGeneralCriteria.getFeedbackType()); + } + + @Test + @DisplayName("Given criteria with boolean values, when created, then all combinations work") + void testBooleanFields() { + FeedbackSearchCriteria trueTrueCriteria = + FeedbackSearchCriteria.builder().isAnonymous(true).isApproved(true).build(); + assertEquals(true, trueTrueCriteria.getIsAnonymous()); + assertEquals(true, trueTrueCriteria.getIsApproved()); + + FeedbackSearchCriteria trueFalseCriteria = + FeedbackSearchCriteria.builder().isAnonymous(true).isApproved(false).build(); + assertEquals(true, trueFalseCriteria.getIsAnonymous()); + assertEquals(false, trueFalseCriteria.getIsApproved()); + + FeedbackSearchCriteria falseTrueCriteria = + FeedbackSearchCriteria.builder().isAnonymous(false).isApproved(true).build(); + assertEquals(false, falseTrueCriteria.getIsAnonymous()); + assertEquals(true, falseTrueCriteria.getIsApproved()); + + FeedbackSearchCriteria falseFalseCriteria = + FeedbackSearchCriteria.builder().isAnonymous(false).isApproved(false).build(); + assertEquals(false, falseFalseCriteria.getIsAnonymous()); + assertEquals(false, falseFalseCriteria.getIsApproved()); + } + + @Test + @DisplayName("Given null values, when equals called, then handles nulls correctly") + void testEqualsWithNulls() { + FeedbackSearchCriteria nullCriteria1 = FeedbackSearchCriteria.builder().build(); + FeedbackSearchCriteria nullCriteria2 = FeedbackSearchCriteria.builder().build(); + + assertEquals(nullCriteria1, nullCriteria2); + } + + @Test + @DisplayName("Given criteria with getters, when called, then returns correct values") + void testGetters() { + assertEquals(1L, criteria.getReviewerId()); + assertEquals(2L, criteria.getRevieweeId()); + assertEquals(3L, criteria.getMentorshipCycleId()); + assertEquals(FeedbackType.MENTOR_REVIEW, criteria.getFeedbackType()); + assertEquals(2026, criteria.getYear()); + assertEquals(false, criteria.getIsAnonymous()); + assertEquals(true, criteria.getIsApproved()); + } +} diff --git a/src/test/java/com/wcc/platform/domain/platform/feedback/FeedbackTest.java b/src/test/java/com/wcc/platform/domain/platform/feedback/FeedbackTest.java new file mode 100644 index 000000000..9b35bed56 --- /dev/null +++ b/src/test/java/com/wcc/platform/domain/platform/feedback/FeedbackTest.java @@ -0,0 +1,275 @@ +package com.wcc.platform.domain.platform.feedback; + +import static com.wcc.platform.factories.SetupFeedbackFactories.createCommunityGeneralFeedbackTest; +import static com.wcc.platform.factories.SetupFeedbackFactories.createMentorReviewFeedbackTest; +import static com.wcc.platform.factories.SetupFeedbackFactories.createMentorshipProgramFeedbackTest; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.wcc.platform.domain.platform.type.FeedbackType; +import java.time.OffsetDateTime; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Test for class {@link Feedback}. */ +class FeedbackTest { + + private Feedback feedback; + + @BeforeEach + void setUp() { + feedback = createMentorReviewFeedbackTest(); + } + + @Test + @DisplayName("Given same feedback, when equals called, then returns true") + void testEquals() { + Feedback sameFeedback = feedback.toBuilder().build(); + assertEquals(feedback, sameFeedback); + } + + @Test + @DisplayName("Given different feedback types, when equals called, then returns false") + void testNotEquals() { + Feedback communityFeedback = createCommunityGeneralFeedbackTest(); + Feedback programFeedback = createMentorshipProgramFeedbackTest(); + Feedback emptyFeedback = new Feedback(); + + assertNotEquals(feedback, communityFeedback); + assertNotEquals(feedback, programFeedback); + assertNotEquals(feedback, emptyFeedback); + } + + @Test + @DisplayName("Given feedback with null field, when equals called, then handles null correctly") + void testEqualsWithNullFields() { + Feedback feedback1 = Feedback.builder().id(1L).reviewerId(1L).build(); + Feedback feedback2 = Feedback.builder().id(1L).reviewerId(1L).build(); + Feedback feedback3 = Feedback.builder().id(2L).reviewerId(1L).build(); + + assertEquals(feedback1, feedback2); + assertNotEquals(feedback1, feedback3); + } + + @Test + @DisplayName("Given same feedback, when hashCode called, then returns same hash") + void testHashCode() { + Feedback sameFeedback = feedback.toBuilder().build(); + assertEquals(feedback.hashCode(), sameFeedback.hashCode()); + } + + @Test + @DisplayName("Given different feedback, when hashCode called, then returns different hash") + void testHashCodeNotEquals() { + Feedback differentFeedback = createCommunityGeneralFeedbackTest(); + assertNotEquals(feedback.hashCode(), differentFeedback.hashCode()); + } + + @Test + @DisplayName("Given feedback with null fields, when hashCode called, then handles null") + void testHashCodeWithNullFields() { + Feedback feedback1 = Feedback.builder().id(1L).build(); + Feedback feedback2 = Feedback.builder().id(1L).build(); + assertEquals(feedback1.hashCode(), feedback2.hashCode()); + } + + @Test + @DisplayName("Given feedback, when toString called, then contains feedback type") + void testToString() { + String result = feedback.toString(); + assertTrue(result.contains(FeedbackType.MENTOR_REVIEW.toString())); + assertTrue(result.contains("reviewerId")); + } + + @Test + @DisplayName("Given feedback, when toString called with null fields, then handles null") + void testToStringWithNullFields() { + Feedback emptyFeedback = new Feedback(); + String result = emptyFeedback.toString(); + assertNotNull(result); + assertTrue(result.contains("Feedback")); + } + + @Test + @DisplayName("Given builder, when building feedback, then all fields set correctly") + void testBuilder() { + Feedback builtFeedback = + Feedback.builder() + .id(10L) + .reviewerId(5L) + .reviewerName("Test Reviewer") + .revieweeId(3L) + .revieweeName("Test Reviewee") + .mentorshipCycleId(2L) + .feedbackType(FeedbackType.MENTORSHIP_PROGRAM) + .rating(4) + .feedbackText("Great program!") + .year(2026) + .isAnonymous(true) + .isApproved(false) + .createdAt(OffsetDateTime.now()) + .updatedAt(OffsetDateTime.now()) + .build(); + + assertEquals(10L, builtFeedback.getId()); + assertEquals(5L, builtFeedback.getReviewerId()); + assertEquals("Test Reviewer", builtFeedback.getReviewerName()); + assertEquals(3L, builtFeedback.getRevieweeId()); + assertEquals("Test Reviewee", builtFeedback.getRevieweeName()); + assertEquals(2L, builtFeedback.getMentorshipCycleId()); + assertEquals(FeedbackType.MENTORSHIP_PROGRAM, builtFeedback.getFeedbackType()); + assertEquals(4, builtFeedback.getRating()); + assertEquals("Great program!", builtFeedback.getFeedbackText()); + assertEquals(2026, builtFeedback.getYear()); + assertTrue(builtFeedback.getIsAnonymous()); + assertFalse(builtFeedback.getIsApproved()); + assertNotNull(builtFeedback.getCreatedAt()); + assertNotNull(builtFeedback.getUpdatedAt()); + } + + @Test + @DisplayName("Given builder with null fields, when building, then creates feedback with nulls") + void testBuilderWithNullFields() { + Feedback minimalFeedback = + Feedback.builder() + .reviewerId(1L) + .feedbackType(FeedbackType.COMMUNITY_GENERAL) + .feedbackText("Test") + .build(); + + assertNull(minimalFeedback.getId()); + assertNull(minimalFeedback.getRevieweeId()); + assertNull(minimalFeedback.getMentorshipCycleId()); + assertNull(minimalFeedback.getRating()); + assertNull(minimalFeedback.getYear()); + assertNull(minimalFeedback.getIsAnonymous()); + assertNull(minimalFeedback.getIsApproved()); + } + + @Test + @DisplayName("Given feedback, when using toBuilder, then creates copy with modifications") + void testToBuilder() { + Feedback modifiedFeedback = + feedback.toBuilder().rating(4).feedbackText("Updated feedback").build(); + + assertEquals(feedback.getId(), modifiedFeedback.getId()); + assertEquals(feedback.getReviewerId(), modifiedFeedback.getReviewerId()); + assertEquals(4, modifiedFeedback.getRating()); + assertEquals("Updated feedback", modifiedFeedback.getFeedbackText()); + } + + @Test + @DisplayName("Given feedback, when setters called, then values updated") + void testSetters() { + // Only test setters that are actually available in production code + feedback.setId(99L); + feedback.setIsAnonymous(false); + feedback.setIsApproved(true); + + assertEquals(99L, feedback.getId()); + assertFalse(feedback.getIsAnonymous()); + assertTrue(feedback.getIsApproved()); + } + + @Test + @DisplayName("Given no-arg constructor, when created, then all fields null") + void testNoArgsConstructor() { + Feedback emptyFeedback = new Feedback(); + + assertNull(emptyFeedback.getId()); + assertNull(emptyFeedback.getReviewerId()); + assertNull(emptyFeedback.getReviewerName()); + assertNull(emptyFeedback.getRevieweeId()); + assertNull(emptyFeedback.getRevieweeName()); + assertNull(emptyFeedback.getMentorshipCycleId()); + assertNull(emptyFeedback.getFeedbackType()); + assertNull(emptyFeedback.getRating()); + assertNull(emptyFeedback.getFeedbackText()); + assertNull(emptyFeedback.getYear()); + assertNull(emptyFeedback.getIsAnonymous()); + assertNull(emptyFeedback.getIsApproved()); + assertNull(emptyFeedback.getCreatedAt()); + assertNull(emptyFeedback.getUpdatedAt()); + } + + @Test + @DisplayName("Given all-args constructor, when created, then all fields set") + void testAllArgsConstructor() { + OffsetDateTime now = OffsetDateTime.now(); + Feedback fullFeedback = + new Feedback( + 1L, + 2L, + "Reviewer Name", + 3L, + "Reviewee Name", + 4L, + FeedbackType.MENTOR_REVIEW, + 5, + "Feedback text", + 2026, + true, + false, + now, + now); + + assertEquals(1L, fullFeedback.getId()); + assertEquals(2L, fullFeedback.getReviewerId()); + assertEquals("Reviewer Name", fullFeedback.getReviewerName()); + assertEquals(3L, fullFeedback.getRevieweeId()); + assertEquals("Reviewee Name", fullFeedback.getRevieweeName()); + assertEquals(4L, fullFeedback.getMentorshipCycleId()); + assertEquals(FeedbackType.MENTOR_REVIEW, fullFeedback.getFeedbackType()); + assertEquals(5, fullFeedback.getRating()); + assertEquals("Feedback text", fullFeedback.getFeedbackText()); + assertEquals(2026, fullFeedback.getYear()); + assertTrue(fullFeedback.getIsAnonymous()); + assertFalse(fullFeedback.getIsApproved()); + assertEquals(now, fullFeedback.getCreatedAt()); + assertEquals(now, fullFeedback.getUpdatedAt()); + } + + @Test + @DisplayName("Given feedback with different rating, when equals called, then returns false") + void testNotEqualsWithDifferentRating() { + Feedback feedback1 = feedback.toBuilder().rating(5).build(); + Feedback feedback2 = feedback.toBuilder().rating(3).build(); + + assertNotEquals(feedback1, feedback2); + } + + @Test + @DisplayName("Given feedback with different anonymous status, when equals, then returns false") + void testNotEqualsWithDifferentAnonymousStatus() { + Feedback feedback1 = feedback.toBuilder().isAnonymous(true).build(); + Feedback feedback2 = feedback.toBuilder().isAnonymous(false).build(); + + assertNotEquals(feedback1, feedback2); + } + + @Test + @DisplayName("Given feedback with different approval status, when equals, then returns false") + void testNotEqualsWithDifferentApprovalStatus() { + Feedback feedback1 = feedback.toBuilder().isApproved(true).build(); + Feedback feedback2 = feedback.toBuilder().isApproved(false).build(); + + assertNotEquals(feedback1, feedback2); + } + + @Test + @DisplayName("Given feedback equals null, when equals called, then returns false") + void testNotEqualsWithNull() { + assertNotEquals(null, feedback); + } + + @Test + @DisplayName("Given feedback equals different class, when equals called, then returns false") + void testNotEqualsWithDifferentClass() { + assertNotEquals("Not a Feedback object", feedback); + } +} diff --git a/src/test/java/com/wcc/platform/domain/platform/feedback/validation/FeedbackDtoValidationTest.java b/src/test/java/com/wcc/platform/domain/platform/feedback/validation/FeedbackDtoValidationTest.java new file mode 100644 index 000000000..f31d9d1e8 --- /dev/null +++ b/src/test/java/com/wcc/platform/domain/platform/feedback/validation/FeedbackDtoValidationTest.java @@ -0,0 +1,311 @@ +package com.wcc.platform.domain.platform.feedback.validation; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.wcc.platform.domain.platform.feedback.FeedbackDto; +import com.wcc.platform.domain.platform.type.FeedbackType; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import java.util.Set; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +/** Test class for FeedbackDto validation. */ +class FeedbackDtoValidationTest { + + private Validator validator; + + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + @DisplayName("Given valid mentor review feedback DTO, when validating, then validation passes") + void testValidMentorReviewFeedback() { + FeedbackDto dto = + FeedbackDto.builder() + .reviewerId(1L) + .revieweeId(2L) + .mentorshipCycleId(1L) + .feedbackType(FeedbackType.MENTOR_REVIEW) + .rating(5) + .feedbackText("Great mentor") + .isAnonymous(false) + .build(); + + Set> violations = validator.validate(dto); + assertTrue(violations.isEmpty(), "Valid MENTOR_REVIEW should have no violations"); + } + + @Test + @DisplayName("Given mentor review without reviewee ID, when validating, then validation fails") + void testMentorReviewWithoutRevieweeIdFails() { + FeedbackDto dto = + FeedbackDto.builder() + .reviewerId(1L) + .mentorshipCycleId(1L) + .feedbackType(FeedbackType.MENTOR_REVIEW) + .rating(5) + .feedbackText("Great mentor") + .isAnonymous(false) + .build(); + + Set> violations = validator.validate(dto); + assertFalse(violations.isEmpty(), "MENTOR_REVIEW without revieweeId should fail"); + assertEquals(1, violations.size()); + assertTrue( + violations.stream() + .anyMatch(v -> v.getMessage().contains("revieweeId is required for MENTOR_REVIEW"))); + } + + @Test + @DisplayName( + "Given mentor review without mentorship cycle ID, when validating, then validation fails") + void testMentorReviewWithoutMentorshipCycleIdFails() { + FeedbackDto dto = + FeedbackDto.builder() + .reviewerId(1L) + .revieweeId(2L) + .feedbackType(FeedbackType.MENTOR_REVIEW) + .rating(5) + .feedbackText("Great mentor") + .isAnonymous(false) + .build(); + + Set> violations = validator.validate(dto); + assertFalse(violations.isEmpty(), "MENTOR_REVIEW without mentorshipCycleId should fail"); + assertEquals(1, violations.size()); + assertTrue( + violations.stream() + .anyMatch(v -> v.getMessage().contains("mentorshipCycleId is required"))); + } + + @Test + @DisplayName( + "Given valid mentorship program feedback DTO, when validating, then validation passes") + void testValidMentorshipProgramFeedback() { + FeedbackDto dto = + FeedbackDto.builder() + .reviewerId(1L) + .mentorshipCycleId(1L) + .feedbackType(FeedbackType.MENTORSHIP_PROGRAM) + .rating(4) + .feedbackText("Great program") + .isAnonymous(true) + .build(); + + Set> violations = validator.validate(dto); + assertTrue(violations.isEmpty(), "Valid MENTORSHIP_PROGRAM should have no violations"); + } + + @Test + @DisplayName("Given mentorship program without cycle ID, when validating, then validation fails") + void testMentorshipProgramWithoutCycleIdFails() { + FeedbackDto dto = + FeedbackDto.builder() + .reviewerId(1L) + .feedbackType(FeedbackType.MENTORSHIP_PROGRAM) + .rating(4) + .feedbackText("Great program") + .isAnonymous(true) + .build(); + + Set> violations = validator.validate(dto); + assertFalse(violations.isEmpty(), "MENTORSHIP_PROGRAM without mentorshipCycleId should fail"); + assertEquals(1, violations.size()); + assertTrue( + violations.stream() + .anyMatch( + v -> + v.getMessage() + .contains("mentorshipCycleId is required for MENTORSHIP_PROGRAM"))); + } + + @Test + @DisplayName("Given mentor review without rating, when validating, then validation fails") + void testMentorReviewWithoutRatingFails() { + FeedbackDto dto = + FeedbackDto.builder() + .reviewerId(1L) + .revieweeId(2L) + .mentorshipCycleId(1L) + .feedbackType(FeedbackType.MENTOR_REVIEW) + .feedbackText("Great mentor") + .isAnonymous(false) + .build(); + + Set> violations = validator.validate(dto); + assertFalse(violations.isEmpty(), "MENTOR_REVIEW without rating should fail"); + assertEquals(1, violations.size()); + assertTrue( + violations.stream() + .anyMatch(v -> v.getMessage().contains("rating is required for MENTOR_REVIEW"))); + } + + @Test + @DisplayName("Given mentorship program without rating, when validating, then validation fails") + void testMentorshipProgramWithoutRatingFails() { + FeedbackDto dto = + FeedbackDto.builder() + .reviewerId(1L) + .mentorshipCycleId(1L) + .feedbackType(FeedbackType.MENTORSHIP_PROGRAM) + .feedbackText("Great program") + .isAnonymous(true) + .build(); + + Set> violations = validator.validate(dto); + assertFalse(violations.isEmpty(), "MENTORSHIP_PROGRAM without rating should fail"); + assertEquals(1, violations.size()); + assertTrue( + violations.stream() + .anyMatch(v -> v.getMessage().contains("rating is required for MENTORSHIP_PROGRAM"))); + } + + @Test + @DisplayName( + "Given valid community general feedback DTO, when validating, then validation passes") + void testValidCommunityGeneralFeedback() { + FeedbackDto dto = + FeedbackDto.builder() + .reviewerId(1L) + .feedbackType(FeedbackType.COMMUNITY_GENERAL) + .rating(5) + .feedbackText("Amazing community") + .isAnonymous(false) + .build(); + + Set> violations = validator.validate(dto); + assertTrue(violations.isEmpty(), "Valid COMMUNITY_GENERAL should have no violations"); + } + + @Test + @DisplayName( + "Given community general feedback without rating, when validating, then validation passes") + void testValidCommunityGeneralFeedbackWithoutRating() { + FeedbackDto dto = + FeedbackDto.builder() + .reviewerId(1L) + .feedbackType(FeedbackType.COMMUNITY_GENERAL) + .feedbackText("Amazing community") + .isAnonymous(false) + .build(); + + Set> violations = validator.validate(dto); + assertTrue( + violations.isEmpty(), + "COMMUNITY_GENERAL without rating should be valid (rating is optional)"); + } + + @Test + @DisplayName("Given DTO with missing required fields, when validating, then validation fails") + void testMissingRequiredFieldsFails() { + FeedbackDto dto = FeedbackDto.builder().build(); + + Set> violations = validator.validate(dto); + assertFalse(violations.isEmpty(), "DTO without required fields should fail"); + // Should have violations for: reviewerId, feedbackType, feedbackText, isAnonymous + assertTrue(violations.size() >= 4); + } + + @Test + @DisplayName("Given DTO with invalid rating value, when validating, then validation fails") + void testInvalidRatingFails() { + FeedbackDto dto = + FeedbackDto.builder() + .reviewerId(1L) + .feedbackType(FeedbackType.COMMUNITY_GENERAL) + .rating(6) // Invalid: must be 1-5 + .feedbackText("Great!") + .isAnonymous(true) + .build(); + + Set> violations = validator.validate(dto); + assertFalse(violations.isEmpty(), "Rating > 5 should fail"); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("rating"))); + } + + @Test + @DisplayName("Given DTO with blank feedback text, when validating, then validation fails") + void testBlankFeedbackTextFails() { + FeedbackDto dto = + FeedbackDto.builder() + .reviewerId(1L) + .feedbackType(FeedbackType.COMMUNITY_GENERAL) + .rating(5) + .feedbackText(" ") // Blank + .isAnonymous(true) + .build(); + + Set> violations = validator.validate(dto); + assertFalse(violations.isEmpty(), "Blank feedback text should fail"); + assertTrue( + violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("feedbackText"))); + } + + @Test + @DisplayName("Given DTO with year below 2000, when validating, then validation fails") + void testYearBelowMinFails() { + FeedbackDto dto = + FeedbackDto.builder() + .reviewerId(1L) + .feedbackType(FeedbackType.COMMUNITY_GENERAL) + .feedbackText("Great!") + .isAnonymous(false) + .year(1999) + .build(); + + Set> violations = validator.validate(dto); + assertFalse(violations.isEmpty(), "Year below 2000 should fail"); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("year"))); + } + + @Test + @DisplayName("Given DTO with year above 2100, when validating, then validation fails") + void testYearAboveMaxFails() { + FeedbackDto dto = + FeedbackDto.builder() + .reviewerId(1L) + .feedbackType(FeedbackType.COMMUNITY_GENERAL) + .feedbackText("Great!") + .isAnonymous(false) + .year(2101) + .build(); + + Set> violations = validator.validate(dto); + assertFalse(violations.isEmpty(), "Year above 2100 should fail"); + assertTrue(violations.stream().anyMatch(v -> v.getPropertyPath().toString().equals("year"))); + } + + @Test + @DisplayName("Given DTO with valid year boundary values, when validating, then validation passes") + void testYearBoundaryValuesPass() { + FeedbackDto dtoMin = + FeedbackDto.builder() + .reviewerId(1L) + .feedbackType(FeedbackType.COMMUNITY_GENERAL) + .feedbackText("Great!") + .isAnonymous(false) + .year(2000) + .build(); + + FeedbackDto dtoMax = + FeedbackDto.builder() + .reviewerId(1L) + .feedbackType(FeedbackType.COMMUNITY_GENERAL) + .feedbackText("Great!") + .isAnonymous(false) + .year(2100) + .build(); + + assertTrue(validator.validate(dtoMin).isEmpty(), "Year 2000 (min boundary) should be valid"); + assertTrue(validator.validate(dtoMax).isEmpty(), "Year 2100 (max boundary) should be valid"); + } +} diff --git a/src/test/java/com/wcc/platform/factories/SetupFeedbackFactories.java b/src/test/java/com/wcc/platform/factories/SetupFeedbackFactories.java new file mode 100644 index 000000000..6ac30082b --- /dev/null +++ b/src/test/java/com/wcc/platform/factories/SetupFeedbackFactories.java @@ -0,0 +1,160 @@ +package com.wcc.platform.factories; + +import com.wcc.platform.domain.platform.feedback.Feedback; +import com.wcc.platform.domain.platform.feedback.FeedbackDto; +import com.wcc.platform.domain.platform.type.FeedbackType; +import java.time.OffsetDateTime; + +/** Setup Factory for Feedback tests. */ +public class SetupFeedbackFactories { + + private static final Long REVIEWER_ID = 1L; + private static final Long REVIEWEE_ID = 2L; + private static final String REVIEWER_NAME = "Mentee Reviewer"; + private static final String REVIEWEE_NAME = "Mentor Reviewee"; + private static final String FEEDBACK_TEXT = "This is a test feedback"; + private static final Integer RATING = 5; + private static final Integer YEAR = 2026; + + /** + * Create a test FeedbackDto for MENTOR_REVIEW type. + * + * @return FeedbackDto + */ + public static FeedbackDto createMentorReviewFeedbackDtoTest() { + return FeedbackDto.builder() + .reviewerId(REVIEWER_ID) + .revieweeId(REVIEWEE_ID) + .mentorshipCycleId(1L) + .feedbackType(FeedbackType.MENTOR_REVIEW) + .rating(RATING) + .feedbackText(FEEDBACK_TEXT) + .year(YEAR) + .isAnonymous(false) + .build(); + } + + /** + * Create a test FeedbackDto for COMMUNITY_GENERAL type. + * + * @return FeedbackDto + */ + public static FeedbackDto createCommunityGeneralFeedbackDtoTest() { + return FeedbackDto.builder() + .reviewerId(REVIEWER_ID) + .feedbackType(FeedbackType.COMMUNITY_GENERAL) + .rating(4) + .feedbackText("Great community experience") + .year(YEAR) + .isAnonymous(true) + .build(); + } + + /** + * Create a test FeedbackDto for MENTORSHIP_PROGRAM type. + * + * @return FeedbackDto + */ + public static FeedbackDto createMentorshipProgramFeedbackDtoTest() { + return FeedbackDto.builder() + .reviewerId(REVIEWER_ID) + .mentorshipCycleId(1L) + .feedbackType(FeedbackType.MENTORSHIP_PROGRAM) + .rating(5) + .feedbackText("Excellent mentorship program") + .year(YEAR) + .isAnonymous(false) + .build(); + } + + /** + * Create a test Feedback domain object for MENTOR_REVIEW type. + * + * @return Feedback + */ + public static Feedback createMentorReviewFeedbackTest() { + return Feedback.builder() + .id(1L) + .reviewerId(REVIEWER_ID) + .reviewerName(REVIEWER_NAME) + .revieweeId(REVIEWEE_ID) + .revieweeName(REVIEWEE_NAME) + .mentorshipCycleId(1L) + .feedbackType(FeedbackType.MENTOR_REVIEW) + .rating(RATING) + .feedbackText(FEEDBACK_TEXT) + .year(YEAR) + .isAnonymous(true) + .isApproved(false) + .createdAt(OffsetDateTime.now()) + .updatedAt(OffsetDateTime.now()) + .build(); + } + + /** + * Create a test Feedback domain object for COMMUNITY_GENERAL type. + * + * @return Feedback + */ + public static Feedback createCommunityGeneralFeedbackTest() { + return Feedback.builder() + .id(2L) + .reviewerId(REVIEWER_ID) + .reviewerName(REVIEWER_NAME) + .feedbackType(FeedbackType.COMMUNITY_GENERAL) + .rating(4) + .feedbackText("Great community experience") + .year(YEAR) + .isAnonymous(false) + .isApproved(true) + .createdAt(OffsetDateTime.now()) + .updatedAt(OffsetDateTime.now()) + .build(); + } + + /** + * Create a test Feedback domain object for MENTORSHIP_PROGRAM type. + * + * @return Feedback + */ + public static Feedback createMentorshipProgramFeedbackTest() { + return Feedback.builder() + .id(3L) + .reviewerId(REVIEWER_ID) + .reviewerName(REVIEWER_NAME) + .mentorshipCycleId(1L) + .feedbackType(FeedbackType.MENTORSHIP_PROGRAM) + .rating(5) + .feedbackText("Excellent mentorship program") + .year(YEAR) + .isAnonymous(false) + .isApproved(true) + .createdAt(OffsetDateTime.now()) + .updatedAt(OffsetDateTime.now()) + .build(); + } + + /** + * Create a test approved and non-anonymous Feedback (reviewer name visible). + * + * @return Feedback + */ + public static Feedback createApprovedPublicFeedbackTest() { + final Feedback feedback = createMentorReviewFeedbackTest(); + feedback.setIsApproved(true); + feedback.setIsAnonymous(false); + return feedback; + } + + /** + * Create a test pending approval Feedback. + * + * @return Feedback + */ + public static Feedback createPendingApprovalFeedbackTest() { + final Feedback feedback = createMentorReviewFeedbackTest(); + feedback.setIsApproved(false); + feedback.setIsAnonymous(true); + return feedback; + } +} diff --git a/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java b/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java new file mode 100644 index 000000000..05449a12c --- /dev/null +++ b/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java @@ -0,0 +1,577 @@ +package com.wcc.platform.repository.postgres; + +import static com.wcc.platform.factories.SetupFeedbackFactories.createMentorReviewFeedbackTest; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.wcc.platform.domain.platform.feedback.Feedback; +import com.wcc.platform.domain.platform.feedback.FeedbackSearchCriteria; +import com.wcc.platform.repository.postgres.component.FeedbackMapper; +import java.util.NoSuchElementException; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.jdbc.core.RowMapper; + +/** PostgresFeedbackRepositoryTest class for testing the PostgresFeedbackRepository. */ +@SuppressWarnings({"PMD.TooManyMethods", "unchecked"}) +class PostgresFeedbackRepositoryTest { + + private static final String DELETE_SQL = "DELETE FROM feedback WHERE id = ?"; + private static final String APPROVE_FEEDBACK = + "UPDATE feedback SET is_approved = true WHERE id = ?"; + private static final String SET_ANONYMOUS_STATUS = + "UPDATE feedback SET is_anonymous = ? WHERE id = ?"; + private static final String GET_ALL_BASE = + "SELECT f.*, m1.full_name AS reviewer_name, m2.full_name AS reviewee_name " + + "FROM feedback f " + + "LEFT JOIN members m1 ON m1.id = f.reviewer_id " + + "LEFT JOIN members m2 ON m2.id = f.reviewee_id " + + "WHERE 1 = 1"; + + private JdbcTemplate jdbc; + private FeedbackMapper feedbackMapper; + private PostgresFeedbackRepository repository; + + @BeforeEach + void setUp() { + jdbc = mock(JdbcTemplate.class); + feedbackMapper = mock(FeedbackMapper.class); + repository = spy(new PostgresFeedbackRepository(jdbc, feedbackMapper)); + } + + @Test + @DisplayName("Given valid feedback, when creating, then returns created feedback with ID") + void testCreate() { + Feedback feedback = createMentorReviewFeedbackTest(); + when(feedbackMapper.addFeedback(any())).thenReturn(1L); + doReturn(Optional.of(feedback)).when(repository).findById(1L); + + Feedback result = repository.create(feedback); + + assertNotNull(result); + assertEquals(1L, result.getId()); + assertEquals("This is a test feedback", result.getFeedbackText()); + verify(feedbackMapper).addFeedback(feedback); + } + + @Test + @DisplayName( + "Given feedback creation with empty findById result, when creating, then throws exception") + void testCreateThrowsWhenFindByIdEmpty() { + Feedback feedback = createMentorReviewFeedbackTest(); + when(feedbackMapper.addFeedback(any())).thenReturn(1L); + doReturn(Optional.empty()).when(repository).findById(1L); + + assertThrows(NoSuchElementException.class, () -> repository.create(feedback)); + } + + @Test + @DisplayName("Given feedback ID and updated data, when updating, then returns updated feedback") + void testUpdate() { + Feedback feedback = + createMentorReviewFeedbackTest().toBuilder().feedbackText("Updated feedback text").build(); + doNothing().when(feedbackMapper).updateFeedback(any(), anyLong()); + doReturn(Optional.of(feedback)).when(repository).findById(1L); + + Feedback result = repository.update(1L, feedback); + + assertNotNull(result); + assertEquals("Updated feedback text", result.getFeedbackText()); + verify(feedbackMapper).updateFeedback(feedback, 1L); + } + + @Test + @DisplayName("Given valid feedback ID, when finding by ID, then returns feedback") + void testFindById() { + Long feedbackId = 1L; + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), (ResultSetExtractor) any(), eq(feedbackId))) + .thenReturn(Optional.of(feedback)); + + Optional result = repository.findById(feedbackId); + + assertNotNull(result); + assertTrue(result.isPresent()); + assertEquals(feedback, result.get()); + assertEquals(1L, result.get().getId()); + } + + @Test + @DisplayName("Given non-existent feedback ID, when finding by ID, then returns empty") + void testFindByIdNotFound() { + Long feedbackId = 999L; + when(jdbc.query(anyString(), (ResultSetExtractor) any(), eq(feedbackId))) + .thenReturn(Optional.empty()); + + Optional result = repository.findById(feedbackId); + + assertNotNull(result); + assertEquals(Optional.empty(), result); + } + + @Test + @DisplayName("Given valid feedback ID, when deleting, then executes delete query") + void testDeleteById() { + Long feedbackId = 1L; + when(jdbc.update(DELETE_SQL, feedbackId)).thenReturn(1); + + repository.deleteById(feedbackId); + + verify(jdbc).update(DELETE_SQL, feedbackId); + } + + @Test + @DisplayName("Given valid feedback ID, when approving, then executes approve query") + void testApproveFeedback() { + Long feedbackId = 1L; + when(jdbc.update(APPROVE_FEEDBACK, feedbackId)).thenReturn(1); + + repository.approveFeedback(feedbackId); + + verify(jdbc).update(APPROVE_FEEDBACK, feedbackId); + } + + @Test + @DisplayName( + "Given feedback ID and anonymous true, when updating anonymous status, then executes update") + void testUpdateAnonymousStatus() { + Long feedbackId = 1L; + Boolean isAnonymous = true; + when(jdbc.update(SET_ANONYMOUS_STATUS, isAnonymous, feedbackId)).thenReturn(1); + + repository.updateAnonymousStatus(feedbackId, isAnonymous); + + verify(jdbc).update(SET_ANONYMOUS_STATUS, isAnonymous, feedbackId); + } + + @Test + @DisplayName( + "Given feedback ID and anonymous false, when updating anonymous status, then executes update") + void testUpdateAnonymousStatusToFalse() { + Long feedbackId = 1L; + Boolean isAnonymous = false; + when(jdbc.update(SET_ANONYMOUS_STATUS, isAnonymous, feedbackId)).thenReturn(1); + + repository.updateAnonymousStatus(feedbackId, isAnonymous); + + verify(jdbc).update(SET_ANONYMOUS_STATUS, isAnonymous, feedbackId); + } + + @Test + @DisplayName("Given JDBC throws exception, when finding by ID, then propagates exception") + void testFindByIdJdbcThrows() { + Long feedbackId = 1L; + when(jdbc.query(anyString(), (ResultSetExtractor) any(), eq(feedbackId))) + .thenThrow(new RuntimeException("DB error")); + + assertThrows(RuntimeException.class, () -> repository.findById(feedbackId)); + } + + @Test + @DisplayName( + "Given invalid search criteria, when getting all, then throws FeedbackNotFoundException") + void testGetAllThrowsFeedbackNotFoundException() { + FeedbackSearchCriteria criteria = FeedbackSearchCriteria.builder().reviewerId(1L).build(); + when(jdbc.query(anyString(), any(RowMapper.class), any())) + .thenThrow( + new com.wcc.platform.domain.exceptions.FeedbackNotFoundException( + "Invalid search criteria")); + + assertThrows( + com.wcc.platform.domain.exceptions.FeedbackNotFoundException.class, + () -> repository.getAll(criteria)); + } + + @Test + @DisplayName("Given non-existent feedback ID, when deleting, then executes delete query") + void testDeleteByIdNonExistent() { + Long feedbackId = 999L; + when(jdbc.update(DELETE_SQL, feedbackId)).thenReturn(0); + + repository.deleteById(feedbackId); + + verify(jdbc).update(DELETE_SQL, feedbackId); + } + + @Test + @DisplayName("Given null criteria, when getting all, then returns all feedback") + void testGetAllNullCriteria() { + when(jdbc.query(anyString(), any(RowMapper.class), any())) + .thenReturn(java.util.Collections.emptyList()); + + var result = repository.getAll(null); + + assertNotNull(result); + verify(jdbc).query(eq(GET_ALL_BASE), any(RowMapper.class), eq(new Object[0])); + } + + @Test + @DisplayName("Given empty criteria, when getting all, then returns empty list") + void testGetAllEmpty() { + FeedbackSearchCriteria criteria = FeedbackSearchCriteria.builder().build(); + when(jdbc.query(anyString(), any(RowMapper.class), any())) + .thenReturn(java.util.Collections.emptyList()); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertTrue(result.isEmpty()); + verify(jdbc).query(eq(GET_ALL_BASE), any(RowMapper.class), eq(new Object[0])); + } + + @Test + @DisplayName("Given multiple search criteria, when getting all, then returns matching feedback") + void testGetAllWithMultipleCriteria() { + Long reviewerId = 1L; + Long revieweeId = 2L; + Integer year = 2026; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder() + .reviewerId(reviewerId) + .revieweeId(revieweeId) + .year(year) + .build(); + + Feedback feedback1 = createMentorReviewFeedbackTest(); + Feedback feedback2 = createMentorReviewFeedbackTest(); + feedback2.setId(2L); + + when(jdbc.query( + eq(GET_ALL_BASE + " AND reviewer_id = ? AND reviewee_id = ? AND feedback_year = ?"), + any(RowMapper.class), + eq(new Object[] {reviewerId, revieweeId, year}))) + .thenReturn(java.util.List.of(feedback1, feedback2)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(2, result.size()); + verify(jdbc) + .query( + eq(GET_ALL_BASE + " AND reviewer_id = ? AND reviewee_id = ? AND feedback_year = ?"), + any(RowMapper.class), + eq(new Object[] {reviewerId, revieweeId, year})); + } + + @Test + @DisplayName("Given reviewer ID filter, when getting all, then returns feedback by reviewer") + void testGetAllWithReviewerIdOnly() { + Long reviewerId = 1L; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().reviewerId(reviewerId).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any())) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq(GET_ALL_BASE + " AND reviewer_id = ?"), + any(RowMapper.class), + eq(new Object[] {reviewerId})); + } + + @Test + @DisplayName("Given reviewee ID filter, when getting all, then returns feedback by reviewee") + void testGetAllWithRevieweeIdOnly() { + Long revieweeId = 2L; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().revieweeId(revieweeId).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any())) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq(GET_ALL_BASE + " AND reviewee_id = ?"), + any(RowMapper.class), + eq(new Object[] {revieweeId})); + } + + @Test + @DisplayName( + "Given feedback type filter, when getting all, then returns feedback of specified type") + void testGetAllWithFeedbackTypeOnly() { + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder() + .feedbackType(com.wcc.platform.domain.platform.type.FeedbackType.MENTOR_REVIEW) + .build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any())) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq(GET_ALL_BASE + " AND feedback_type_id = ?"), + any(RowMapper.class), + eq(new Object[] {1})); + } + + @Test + @DisplayName("Given year filter, when getting all, then returns feedback for specified year") + void testGetAllWithYearOnly() { + Integer year = 2026; + FeedbackSearchCriteria criteria = FeedbackSearchCriteria.builder().year(year).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any())) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq(GET_ALL_BASE + " AND feedback_year = ?"), + any(RowMapper.class), + eq(new Object[] {year})); + } + + @Test + @DisplayName( + "Given mentorship cycle ID filter, when getting all, then returns feedback for cycle") + void testGetAllWithMentorshipCycleIdOnly() { + Long mentorshipCycleId = 5L; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().mentorshipCycleId(mentorshipCycleId).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any())) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq(GET_ALL_BASE + " AND mentorship_cycle_id = ?"), + any(RowMapper.class), + eq(new Object[] {mentorshipCycleId})); + } + + @Test + @DisplayName("Given isApproved true filter, when getting all, then returns approved feedback") + void testGetAllWithIsApprovedTrue() { + Boolean isApproved = true; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().isApproved(isApproved).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any())) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq(GET_ALL_BASE + " AND is_approved = ?"), + any(RowMapper.class), + eq(new Object[] {isApproved})); + } + + @Test + @DisplayName("Given isApproved false filter, when getting all, then returns unapproved feedback") + void testGetAllWithIsApprovedFalse() { + Boolean isApproved = false; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().isApproved(isApproved).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any())) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq(GET_ALL_BASE + " AND is_approved = ?"), + any(RowMapper.class), + eq(new Object[] {isApproved})); + } + + @Test + @DisplayName("Given isAnonymous true filter, when getting all, then returns anonymous feedback") + void testGetAllWithIsAnonymousTrue() { + Boolean isAnonymous = true; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().isAnonymous(isAnonymous).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any())) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq(GET_ALL_BASE + " AND is_anonymous = ?"), + any(RowMapper.class), + eq(new Object[] {isAnonymous})); + } + + @Test + @DisplayName( + "Given isAnonymous false filter, when getting all, then returns non-anonymous feedback") + void testGetAllWithIsAnonymousFalse() { + Boolean isAnonymous = false; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().isAnonymous(isAnonymous).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any())) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq(GET_ALL_BASE + " AND is_anonymous = ?"), + any(RowMapper.class), + eq(new Object[] {isAnonymous})); + } + + @Test + @DisplayName( + "Given all search criteria, when getting all, then returns feedback matching all filters") + void testGetAllWithAllCriteria() { + Long reviewerId = 1L; + Long revieweeId = 2L; + Long mentorshipCycleId = 3L; + Integer year = 2026; + Boolean isApproved = true; + Boolean isAnonymous = false; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder() + .reviewerId(reviewerId) + .revieweeId(revieweeId) + .feedbackType(com.wcc.platform.domain.platform.type.FeedbackType.MENTOR_REVIEW) + .year(year) + .mentorshipCycleId(mentorshipCycleId) + .isApproved(isApproved) + .isAnonymous(isAnonymous) + .build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query( + eq( + GET_ALL_BASE + + " AND reviewer_id = ? AND reviewee_id = ?" + + " AND feedback_type_id = ? AND feedback_year = ? AND mentorship_cycle_id = ?" + + " AND is_approved = ? AND is_anonymous = ?"), + any(RowMapper.class), + eq( + new Object[] { + reviewerId, revieweeId, 1, year, mentorshipCycleId, isApproved, isAnonymous + }))) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq( + GET_ALL_BASE + + " AND reviewer_id = ? AND reviewee_id = ?" + + " AND feedback_type_id = ? AND feedback_year = ? AND mentorship_cycle_id = ?" + + " AND is_approved = ? AND is_anonymous = ?"), + any(RowMapper.class), + eq( + new Object[] { + reviewerId, revieweeId, 1, year, mentorshipCycleId, isApproved, isAnonymous + })); + } + + @Test + @DisplayName( + "Given feedback type and year filters, when getting all, then returns matching feedback") + void testGetAllWithFeedbackTypeAndYear() { + Integer year = 2026; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder() + .feedbackType(com.wcc.platform.domain.platform.type.FeedbackType.COMMUNITY_GENERAL) + .year(year) + .build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any())) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + verify(jdbc) + .query( + eq(GET_ALL_BASE + " AND feedback_type_id = ? AND feedback_year = ?"), + any(RowMapper.class), + eq(new Object[] {2, year})); + } + + @Test + @DisplayName( + "Given mentorship cycle and approved filters, when getting all," + + "then returns matching feedback") + void testGetAllWithMentorshipCycleAndApproved() { + Long mentorshipCycleId = 10L; + Boolean isApproved = true; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder() + .mentorshipCycleId(mentorshipCycleId) + .isApproved(isApproved) + .build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any())) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + verify(jdbc) + .query( + eq(GET_ALL_BASE + " AND mentorship_cycle_id = ? AND is_approved = ?"), + any(RowMapper.class), + eq(new Object[] {mentorshipCycleId, isApproved})); + } +} diff --git a/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java b/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java new file mode 100644 index 000000000..515f81a8d --- /dev/null +++ b/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java @@ -0,0 +1,268 @@ +package com.wcc.platform.repository.postgres.component; + +import static com.wcc.platform.repository.postgres.component.FeedbackMapper.INSERT_SQL; +import static com.wcc.platform.repository.postgres.component.FeedbackMapper.UPDATE_SQL; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_CREATED_AT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_FB_TYPE_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_FEEDBACK_TEXT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_IS_ANONYMOUS; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_IS_APPROVED; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_MS_CYCLE_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_RATING; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_REVIEWEE_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_REVIEWEE_NAME; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_REVIEWER_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_REVIEWER_NAME; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_UPDATED_AT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_YEAR; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.wcc.platform.domain.platform.feedback.Feedback; +import com.wcc.platform.domain.platform.type.FeedbackType; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.OffsetDateTime; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.jdbc.core.JdbcTemplate; + +/** FeedbackMapperTest class for testing the FeedbackMapper. */ +class FeedbackMapperTest { + + @Mock private JdbcTemplate jdbc; + @Mock private ResultSet resultSet; + + private FeedbackMapper feedbackMapper; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + feedbackMapper = new FeedbackMapper(jdbc); + } + + @Test + @DisplayName( + "Given ResultSet with all fields, when mapping row to feedback, " + + "then returns complete feedback") + void testMapRowToFeedback() throws SQLException { + Long feedbackId = 1L; + Long reviewerId = 10L; + Long revieweeId = 20L; + Long mentorshipCycleId = 5L; + Integer feedbackTypeId = 1; // MENTOR_REVIEW + Integer rating = 5; + String feedbackText = "Excellent mentor!"; + Integer year = 2026; + Boolean isAnonymous = false; + Boolean isApproved = true; + OffsetDateTime createdAt = OffsetDateTime.now(); + OffsetDateTime updatedAt = OffsetDateTime.now(); + + when(resultSet.getLong(COL_ID)).thenReturn(feedbackId); + when(resultSet.getLong(COL_REVIEWER_ID)).thenReturn(reviewerId); + when(resultSet.getString(COL_REVIEWER_NAME)).thenReturn("Reviewer Name"); + when(resultSet.getObject(COL_REVIEWEE_ID)).thenReturn(revieweeId); + when(resultSet.getLong(COL_REVIEWEE_ID)).thenReturn(revieweeId); + when(resultSet.getString(COL_REVIEWEE_NAME)).thenReturn("Reviewee Name"); + when(resultSet.getObject(COL_MS_CYCLE_ID)).thenReturn(mentorshipCycleId); + when(resultSet.getLong(COL_MS_CYCLE_ID)).thenReturn(mentorshipCycleId); + when(resultSet.getInt(COL_FB_TYPE_ID)).thenReturn(feedbackTypeId); + when(resultSet.getObject(COL_RATING)).thenReturn(rating); + when(resultSet.getInt(COL_RATING)).thenReturn(rating); + when(resultSet.getString(COL_FEEDBACK_TEXT)).thenReturn(feedbackText); + when(resultSet.getObject(COL_YEAR)).thenReturn(year); + when(resultSet.getInt(COL_YEAR)).thenReturn(year); + when(resultSet.getBoolean(COL_IS_ANONYMOUS)).thenReturn(isAnonymous); + when(resultSet.getBoolean(COL_IS_APPROVED)).thenReturn(isApproved); + when(resultSet.getObject(COL_CREATED_AT)).thenReturn(createdAt); + when(resultSet.getObject(COL_CREATED_AT, OffsetDateTime.class)).thenReturn(createdAt); + when(resultSet.getObject(COL_UPDATED_AT)).thenReturn(updatedAt); + when(resultSet.getObject(COL_UPDATED_AT, OffsetDateTime.class)).thenReturn(updatedAt); + + Feedback feedback = feedbackMapper.mapRowToFeedback(resultSet); + + assertNotNull(feedback); + assertEquals(feedbackId, feedback.getId()); + assertEquals(reviewerId, feedback.getReviewerId()); + assertEquals(revieweeId, feedback.getRevieweeId()); + assertEquals(mentorshipCycleId, feedback.getMentorshipCycleId()); + assertEquals(FeedbackType.MENTOR_REVIEW, feedback.getFeedbackType()); + assertEquals(rating, feedback.getRating()); + assertEquals(feedbackText, feedback.getFeedbackText()); + assertEquals(year, feedback.getYear()); + assertEquals(isAnonymous, feedback.getIsAnonymous()); + assertEquals(isApproved, feedback.getIsApproved()); + assertEquals(createdAt, feedback.getCreatedAt()); + assertEquals(updatedAt, feedback.getUpdatedAt()); + assertEquals("Reviewer Name", feedback.getReviewerName()); + assertEquals("Reviewee Name", feedback.getRevieweeName()); + } + + @Test + @DisplayName( + "Given ResultSet with nullable fields, when mapping row to feedback, " + + "then returns feedback with nulls") + void testMapRowToFeedbackWithNullableFields() throws SQLException { + Long feedbackId = 2L; + Long reviewerId = 10L; + Integer feedbackTypeId = 2; // COMMUNITY_GENERAL + String feedbackText = "Great community!"; + Boolean isAnonymous = true; + Boolean isApproved = false; + + when(resultSet.getLong(COL_ID)).thenReturn(feedbackId); + when(resultSet.getLong(COL_REVIEWER_ID)).thenReturn(reviewerId); + when(resultSet.getString(COL_REVIEWER_NAME)).thenReturn("Reviewer Name"); + when(resultSet.getObject(COL_REVIEWEE_ID)).thenReturn(null); + when(resultSet.getString(COL_REVIEWEE_NAME)).thenReturn(null); + when(resultSet.getObject(COL_MS_CYCLE_ID)).thenReturn(null); + when(resultSet.getInt(COL_FB_TYPE_ID)).thenReturn(feedbackTypeId); + when(resultSet.getObject(COL_RATING)).thenReturn(null); + when(resultSet.getString(COL_FEEDBACK_TEXT)).thenReturn(feedbackText); + when(resultSet.getObject(COL_YEAR)).thenReturn(null); + when(resultSet.getBoolean(COL_IS_ANONYMOUS)).thenReturn(isAnonymous); + when(resultSet.getBoolean(COL_IS_APPROVED)).thenReturn(isApproved); + when(resultSet.getObject(COL_CREATED_AT)).thenReturn(null); + when(resultSet.getObject(COL_UPDATED_AT)).thenReturn(null); + + Feedback feedback = feedbackMapper.mapRowToFeedback(resultSet); + + assertNotNull(feedback); + assertEquals(feedbackId, feedback.getId()); + assertEquals(reviewerId, feedback.getReviewerId()); + assertNull(feedback.getRevieweeId()); + assertNull(feedback.getMentorshipCycleId()); + assertEquals(FeedbackType.COMMUNITY_GENERAL, feedback.getFeedbackType()); + assertNull(feedback.getRating()); + assertEquals(feedbackText, feedback.getFeedbackText()); + assertNull(feedback.getYear()); + assertEquals(isAnonymous, feedback.getIsAnonymous()); + assertEquals(isApproved, feedback.getIsApproved()); + assertNull(feedback.getCreatedAt()); + assertNull(feedback.getUpdatedAt()); + } + + @Test + @DisplayName("Given ResultSet throws SQLException, when mapping, then propagates exception") + void handlesSqlExceptionGracefully() throws Exception { + when(resultSet.getLong(COL_ID)).thenThrow(SQLException.class); + assertThrows(SQLException.class, () -> feedbackMapper.mapRowToFeedback(resultSet)); + } + + @Test + @DisplayName("Given feedback with all fields, when adding, then inserts and returns ID") + void testAddFeedback() { + Feedback feedback = + Feedback.builder() + .reviewerId(1L) + .revieweeId(2L) + .mentorshipCycleId(5L) + .feedbackType(FeedbackType.MENTOR_REVIEW) + .rating(5) + .feedbackText("Excellent mentor!") + .year(2026) + .isAnonymous(true) + .isApproved(false) + .build(); + + when(jdbc.queryForObject( + INSERT_SQL, Long.class, 1L, 2L, 5L, 1, 5, "Excellent mentor!", 2026, true, false)) + .thenReturn(100L); + + Long feedbackId = feedbackMapper.addFeedback(feedback); + + assertEquals(100L, feedbackId); + verify(jdbc) + .queryForObject( + INSERT_SQL, Long.class, 1L, 2L, 5L, 1, 5, "Excellent mentor!", 2026, true, false); + } + + @Test + @DisplayName( + "Given feedback with nullable fields, when adding, then inserts with nulls and returns ID") + void testAddFeedbackWithNullableFields() { + Feedback feedback = + Feedback.builder() + .reviewerId(1L) + .feedbackType(FeedbackType.COMMUNITY_GENERAL) + .feedbackText("Great community!") + .isAnonymous(false) + .isApproved(true) + .build(); + + when(jdbc.queryForObject( + INSERT_SQL, Long.class, 1L, null, null, 2, null, "Great community!", null, false, true)) + .thenReturn(200L); + + Long feedbackId = feedbackMapper.addFeedback(feedback); + + assertEquals(200L, feedbackId); + verify(jdbc) + .queryForObject( + INSERT_SQL, Long.class, 1L, null, null, 2, null, "Great community!", null, false, true); + } + + @Test + @DisplayName("Given feedback with all fields, when updating, then executes update query") + void testUpdateFeedback() { + Long feedbackId = 1L; + Feedback feedback = + Feedback.builder() + .reviewerId(1L) + .revieweeId(2L) + .mentorshipCycleId(5L) + .feedbackType(FeedbackType.MENTOR_REVIEW) + .rating(4) + .feedbackText("Updated feedback text") + .year(2026) + .isAnonymous(false) + .isApproved(true) + .build(); + + feedbackMapper.updateFeedback(feedback, feedbackId); + + verify(jdbc) + .update( + UPDATE_SQL, 1L, 2L, 5L, 1, 4, "Updated feedback text", 2026, false, true, feedbackId); + } + + @Test + @DisplayName( + "Given feedback with nullable fields, when updating, then executes update query with nulls") + void testUpdateFeedbackWithNullableFields() { + Long feedbackId = 2L; + Feedback feedback = + Feedback.builder() + .reviewerId(10L) + .feedbackType(FeedbackType.MENTORSHIP_PROGRAM) + .feedbackText("Updated program feedback") + .isAnonymous(true) + .isApproved(false) + .build(); + + feedbackMapper.updateFeedback(feedback, feedbackId); + + verify(jdbc) + .update( + UPDATE_SQL, + 10L, + null, + null, + 3, + null, + "Updated program feedback", + null, + true, + false, + feedbackId); + } +} diff --git a/src/test/java/com/wcc/platform/service/FeedbackServiceTest.java b/src/test/java/com/wcc/platform/service/FeedbackServiceTest.java new file mode 100644 index 000000000..7c892b71c --- /dev/null +++ b/src/test/java/com/wcc/platform/service/FeedbackServiceTest.java @@ -0,0 +1,331 @@ +package com.wcc.platform.service; + +import static com.wcc.platform.factories.SetupFactories.createMemberTest; +import static com.wcc.platform.factories.SetupFeedbackFactories.createCommunityGeneralFeedbackDtoTest; +import static com.wcc.platform.factories.SetupFeedbackFactories.createCommunityGeneralFeedbackTest; +import static com.wcc.platform.factories.SetupFeedbackFactories.createMentorReviewFeedbackDtoTest; +import static com.wcc.platform.factories.SetupFeedbackFactories.createMentorReviewFeedbackTest; +import static com.wcc.platform.factories.SetupFeedbackFactories.createMentorshipProgramFeedbackDtoTest; +import static com.wcc.platform.factories.SetupFeedbackFactories.createMentorshipProgramFeedbackTest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.wcc.platform.domain.exceptions.FeedbackNotFoundException; +import com.wcc.platform.domain.exceptions.MemberNotFoundException; +import com.wcc.platform.domain.platform.feedback.Feedback; +import com.wcc.platform.domain.platform.feedback.FeedbackDto; +import com.wcc.platform.domain.platform.feedback.FeedbackSearchCriteria; +import com.wcc.platform.domain.platform.member.Member; +import com.wcc.platform.repository.FeedbackRepository; +import com.wcc.platform.repository.MemberRepository; +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.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@DisplayName("Feedback Service Tests") +class FeedbackServiceTest { + @Mock private FeedbackRepository feedbackRepository; + @Mock private MemberRepository memberRepository; + private FeedbackService service; + private Feedback feedback; + private Feedback communityFeedback; + private Feedback mentorshipFeedback; + private FeedbackDto feedbackDto; + private FeedbackDto communityFeedbackDto; + private FeedbackDto mentorshipFeedbackDto; + private Member reviewer; + private Member reviewee; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + service = new FeedbackService(feedbackRepository, memberRepository); + + feedback = createMentorReviewFeedbackTest(); + communityFeedback = createCommunityGeneralFeedbackTest(); + mentorshipFeedback = createMentorshipProgramFeedbackTest(); + feedbackDto = createMentorReviewFeedbackDtoTest(); + communityFeedbackDto = createCommunityGeneralFeedbackDtoTest(); + mentorshipFeedbackDto = createMentorshipProgramFeedbackDtoTest(); + reviewer = createMemberTest(com.wcc.platform.domain.platform.type.MemberType.MENTOR); + reviewer.setId(feedbackDto.getReviewerId()); + reviewee = createMemberTest(com.wcc.platform.domain.platform.type.MemberType.MENTEE); + reviewee.setId(feedbackDto.getRevieweeId()); + } + + @Test + @DisplayName("Should create feedback successfully") + void testCreateFeedback() { + when(memberRepository.findById(feedbackDto.getReviewerId())).thenReturn(Optional.of(reviewer)); + when(memberRepository.findById(feedbackDto.getRevieweeId())).thenReturn(Optional.of(reviewee)); + when(feedbackRepository.create(any(Feedback.class))).thenReturn(feedback); + + final Feedback result = service.createFeedback(feedbackDto); + + assertEquals(feedback, result); + assertThat(result.getReviewerId()).isEqualTo(feedbackDto.getReviewerId()); + verify(memberRepository).findById(feedbackDto.getReviewerId()); + verify(memberRepository).findById(feedbackDto.getRevieweeId()); + verify(feedbackRepository).create(any(Feedback.class)); + } + + @Test + @DisplayName("Should create community general feedback without reviewee") + void testCreateCommunityGeneralFeedbackNoReviewee() { + when(memberRepository.findById(communityFeedbackDto.getReviewerId())) + .thenReturn(Optional.of(reviewer)); + when(feedbackRepository.create(any(Feedback.class))).thenReturn(communityFeedback); + + final Feedback result = service.createFeedback(communityFeedbackDto); + + assertEquals(communityFeedback, result); + assertThat(result.getRevieweeId()).isNull(); + verify(memberRepository).findById(anyLong()); + verify(feedbackRepository).create(any(Feedback.class)); + } + + @Test + @DisplayName("Should create mentorship program feedback") + void testCreateMentorshipProgramFeedbackSuccess() { + when(memberRepository.findById(mentorshipFeedbackDto.getReviewerId())) + .thenReturn(Optional.of(reviewer)); + when(feedbackRepository.create(any(Feedback.class))).thenReturn(mentorshipFeedback); + + final Feedback result = service.createFeedback(mentorshipFeedbackDto); + + assertEquals(mentorshipFeedback, result); + assertThat(result.getMentorshipCycleId()).isNotNull(); + assertThat(result.getRevieweeId()).isNull(); + verify(memberRepository).findById(anyLong()); + verify(feedbackRepository).create(any(Feedback.class)); + } + + @Test + @DisplayName("Should throw MemberNotFoundException when reviewer not found") + void testCreateFeedbackReviewerNotFound() { + when(memberRepository.findById(feedbackDto.getReviewerId())).thenReturn(Optional.empty()); + + assertThrows(MemberNotFoundException.class, () -> service.createFeedback(feedbackDto)); + verify(feedbackRepository, never()).create(any(Feedback.class)); + } + + @Test + @DisplayName("Should throw MemberNotFoundException when reviewee not found") + void testCreateFeedbackRevieweeNotFound() { + when(memberRepository.findById(feedbackDto.getReviewerId())).thenReturn(Optional.of(reviewer)); + when(memberRepository.findById(feedbackDto.getRevieweeId())).thenReturn(Optional.empty()); + + assertThrows(MemberNotFoundException.class, () -> service.createFeedback(feedbackDto)); + verify(feedbackRepository, never()).create(any(Feedback.class)); + } + + @Test + @DisplayName("Should update feedback successfully") + void testUpdateFeedback() { + final Long feedbackId = 1L; + when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.of(feedback)); + when(memberRepository.findById(feedbackDto.getReviewerId())).thenReturn(Optional.of(reviewer)); + when(memberRepository.findById(feedbackDto.getRevieweeId())).thenReturn(Optional.of(reviewee)); + when(feedbackRepository.update(eq(feedbackId), any(Feedback.class))).thenReturn(feedback); + + Feedback result = service.updateFeedback(feedbackId, feedbackDto); + + assertThat(result.getId()).isEqualTo(feedbackId); + verify(feedbackRepository).findById(feedbackId); + verify(memberRepository).findById(feedbackDto.getReviewerId()); + verify(memberRepository).findById(feedbackDto.getRevieweeId()); + verify(feedbackRepository).update(eq(feedbackId), any(Feedback.class)); + } + + @Test + @DisplayName("Should throw FeedbackNotFoundException when updating non-existent feedback") + void testUpdateFeedbackNotFound() { + final Long feedbackId = 999L; + when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.empty()); + + assertThrows( + FeedbackNotFoundException.class, () -> service.updateFeedback(feedbackId, feedbackDto)); + verify(feedbackRepository, never()).update(anyLong(), any(Feedback.class)); + } + + @Test + @DisplayName("Should get feedback by ID successfully") + void testGetFeedbackById() { + final Long feedbackId = 1L; + when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.of(feedback)); + + final Feedback result = service.getFeedbackById(feedbackId); + assertThat(result.getId()).isEqualTo(feedbackId); + assertEquals(feedback, result); + verify(feedbackRepository).findById(feedbackId); + } + + @Test + @DisplayName("Should throw FeedbackNotFoundException when getting non-existent feedback") + void testGetFeedbackByIdNotFound() { + final Long feedbackId = 999L; + when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.empty()); + + assertThrows(FeedbackNotFoundException.class, () -> service.getFeedbackById(feedbackId)); + } + + @Test + @DisplayName("Should approve feedback successfully") + void testApproveFeedback() { + final Long feedbackId = 1L; + when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.of(feedback)); + + service.approveFeedback(feedbackId); + + verify(feedbackRepository).approveFeedback(feedbackId); + } + + @Test + @DisplayName("Should throw FeedbackNotFoundException when approving non-existent feedback") + void testApproveFeedbackNotFound() { + final Long feedbackId = 999L; + when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.empty()); + + assertThrows(FeedbackNotFoundException.class, () -> service.approveFeedback(feedbackId)); + verify(feedbackRepository, never()).approveFeedback(anyLong()); + } + + @Test + @DisplayName("Should set feedback anonymous status successfully") + void testUpdateFeedbackAnonymousStatus() { + final Long feedbackId = 1L; + when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.of(feedback)); + + service.updateFeedbackAnonymousStatus(feedbackId, true); + + verify(feedbackRepository, times(1)).updateAnonymousStatus(feedbackId, true); + } + + @Test + @DisplayName("Should delete feedback successfully") + void testDeleteFeedback() { + final Long feedbackId = 1L; + when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.of(feedback)); + + service.deleteFeedback(feedbackId); + + verify(feedbackRepository).deleteById(feedbackId); + } + + @Test + @DisplayName("Should throw FeedbackNotFoundException when deleting non-existent feedback") + void testDeleteFeedbackNotFound() { + final Long feedbackId = 999L; + when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.empty()); + + assertThrows(FeedbackNotFoundException.class, () -> service.deleteFeedback(feedbackId)); + verify(feedbackRepository, never()).deleteById(anyLong()); + } + + @Test + @DisplayName("Should get all feedback with null criteria") + void testGetAllFeedbackNullCriteria() { + Feedback feedback1 = createMentorReviewFeedbackTest(); + Feedback feedback2 = createCommunityGeneralFeedbackTest(); + List expectedList = List.of(feedback1, feedback2); + + when(feedbackRepository.getAll(null)).thenReturn(expectedList); + + List result = service.getAllFeedback(null); + + assertThat(result).isNotNull().hasSize(2); + assertEquals(expectedList, result); + verify(feedbackRepository).getAll(null); + } + + @Test + @DisplayName("Should get all feedback with empty criteria") + void testGetAllFeedbackEmptyCriteria() { + FeedbackSearchCriteria criteria = FeedbackSearchCriteria.builder().build(); + Feedback feedback1 = createMentorReviewFeedbackTest(); + Feedback feedback2 = createMentorshipProgramFeedbackTest(); + List expectedList = List.of(feedback1, feedback2); + + when(feedbackRepository.getAll(criteria)).thenReturn(expectedList); + + List result = service.getAllFeedback(criteria); + + assertThat(result).isNotNull().hasSize(2); + assertEquals(expectedList, result); + verify(feedbackRepository).getAll(criteria); + } + + @Test + @DisplayName("Should get all feedback with multiple criteria") + void testGetAllFeedbackMultipleCriteria() { + Long reviewerId = 1L; + Long revieweeId = 2L; + Integer year = 2026; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder() + .reviewerId(reviewerId) + .revieweeId(revieweeId) + .year(year) + .build(); + + Feedback feedback1 = createMentorReviewFeedbackTest(); + Feedback feedback2 = createMentorReviewFeedbackTest(); + feedback2.setId(2L); + List expectedList = List.of(feedback1, feedback2); + + when(memberRepository.findById(reviewerId)).thenReturn(Optional.of(reviewer)); + when(memberRepository.findById(revieweeId)).thenReturn(Optional.of(reviewee)); + when(feedbackRepository.getAll(criteria)).thenReturn(expectedList); + + List result = service.getAllFeedback(criteria); + + assertThat(result).isNotNull().hasSize(2); + assertEquals(expectedList, result); + verify(memberRepository).findById(reviewerId); + verify(memberRepository).findById(revieweeId); + verify(feedbackRepository).getAll(criteria); + } + + @Test + @DisplayName("Should throw MemberNotFoundException when reviewer not found in getAll") + void testGetAllFeedbackReviewerNotFound() { + Long reviewerId = 999L; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().reviewerId(reviewerId).build(); + + when(memberRepository.findById(reviewerId)).thenReturn(Optional.empty()); + + assertThrows(MemberNotFoundException.class, () -> service.getAllFeedback(criteria)); + verify(memberRepository).findById(reviewerId); + verify(feedbackRepository, never()).getAll(any()); + } + + @Test + @DisplayName("Should throw MemberNotFoundException when reviewee not found in getAll") + void testGetAllFeedbackRevieweeNotFound() { + Long reviewerId = 1L; + Long revieweeId = 999L; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().reviewerId(reviewerId).revieweeId(revieweeId).build(); + + when(memberRepository.findById(reviewerId)).thenReturn(Optional.of(reviewer)); + when(memberRepository.findById(revieweeId)).thenReturn(Optional.empty()); + + assertThrows(MemberNotFoundException.class, () -> service.getAllFeedback(criteria)); + verify(memberRepository).findById(reviewerId); + verify(memberRepository).findById(revieweeId); + verify(feedbackRepository, never()).getAll(any()); + } +}