From a4a1986c28af4805ef6a57c231a135c0ccb9b687 Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Tue, 17 Mar 2026 11:10:08 +0100 Subject: [PATCH 01/17] Feedback API Implement API for Community, Mentorship and Mentor feedback. --- feedback-post-request.json | 8 + .../configuration/GlobalExceptionHandler.java | 4 +- .../controller/FeedbackController.java | 171 +++++++ .../exceptions/FeedbackNotFoundException.java | 16 + .../domain/platform/feedback/Feedback.java | 39 ++ .../domain/platform/feedback/FeedbackDto.java | 54 +++ .../feedback/FeedbackSearchCriteria.java | 21 + .../validation/FeedbackValidator.java | 56 +++ .../feedback/validation/ValidFeedback.java | 20 + .../domain/platform/type/FeedbackType.java | 33 ++ .../repository/FeedbackRepository.java | 35 ++ .../postgres/PostgresFeedbackRepository.java | 114 +++++ .../postgres/component/FeedbackMapper.java | 113 +++++ .../postgres/constants/FeedbackConstants.java | 21 + .../wcc/platform/service/FeedbackService.java | 166 +++++++ src/main/resources/init-data/feedback.json | 81 ++++ .../controller/FeedbackControllerTest.java | 433 ++++++++++++++++++ .../validation/FeedbackDtoValidationTest.java | 235 ++++++++++ .../factories/SetupFeedbackFactories.java | 160 +++++++ .../PostgresFeedbackRepositoryTest.java | 233 ++++++++++ .../component/FeedbackMapperTest.java | 292 ++++++++++++ .../platform/service/FeedbackServiceTest.java | 334 ++++++++++++++ 22 files changed, 2638 insertions(+), 1 deletion(-) create mode 100644 feedback-post-request.json create mode 100644 src/main/java/com/wcc/platform/controller/FeedbackController.java create mode 100644 src/main/java/com/wcc/platform/domain/exceptions/FeedbackNotFoundException.java create mode 100644 src/main/java/com/wcc/platform/domain/platform/feedback/Feedback.java create mode 100644 src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackDto.java create mode 100644 src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackSearchCriteria.java create mode 100644 src/main/java/com/wcc/platform/domain/platform/feedback/validation/FeedbackValidator.java create mode 100644 src/main/java/com/wcc/platform/domain/platform/feedback/validation/ValidFeedback.java create mode 100644 src/main/java/com/wcc/platform/domain/platform/type/FeedbackType.java create mode 100644 src/main/java/com/wcc/platform/repository/FeedbackRepository.java create mode 100644 src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java create mode 100644 src/main/java/com/wcc/platform/repository/postgres/component/FeedbackMapper.java create mode 100644 src/main/java/com/wcc/platform/repository/postgres/constants/FeedbackConstants.java create mode 100644 src/main/java/com/wcc/platform/service/FeedbackService.java create mode 100644 src/main/resources/init-data/feedback.json create mode 100644 src/test/java/com/wcc/platform/controller/FeedbackControllerTest.java create mode 100644 src/test/java/com/wcc/platform/domain/platform/feedback/validation/FeedbackDtoValidationTest.java create mode 100644 src/test/java/com/wcc/platform/factories/SetupFeedbackFactories.java create mode 100644 src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java create mode 100644 src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java create mode 100644 src/test/java/com/wcc/platform/service/FeedbackServiceTest.java diff --git a/feedback-post-request.json b/feedback-post-request.json new file mode 100644 index 000000000..d1c94b6ba --- /dev/null +++ b/feedback-post-request.json @@ -0,0 +1,8 @@ +{ + "reviewerId": 1, + "feedbackType": "COMMUNITY_GENERAL", + "rating": 4, + "feedbackText": "Great community with excellent mentors and structured programs. The mentorship cycle is well organized and everyone is supportive.", + "year": 2026 +} + diff --git a/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java b/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java index f76af2acd..a1dbfaf29 100644 --- a/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java +++ b/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java @@ -8,6 +8,7 @@ import com.wcc.platform.domain.exceptions.DuplicatedMemberException; 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.InvalidProgramTypeException; import com.wcc.platform.domain.exceptions.MemberNotFoundException; import com.wcc.platform.domain.exceptions.MentorshipCycleClosedException; @@ -31,7 +32,8 @@ public class GlobalExceptionHandler { @ExceptionHandler({ ContentNotFoundException.class, NoSuchElementException.class, - MemberNotFoundException.class + MemberNotFoundException.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..5bdd90e7f --- /dev/null +++ b/src/main/java/com/wcc/platform/controller/FeedbackController.java @@ -0,0 +1,171 @@ +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 java.util.List; +import lombok.AllArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +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 +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) 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(@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, + @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 + */ + @PutMapping("/{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 + */ + @PutMapping("/{feedbackId}/anonymous-status") + @Operation(summary = "Set feedback anonymous status (to hide/show reviewer name)") + @ResponseStatus(HttpStatus.OK) + public ResponseEntity setFeedbackAnonymousStatus( + @Parameter(description = "ID of the feedback") @PathVariable final Long feedbackId, + @Parameter(description = "Is anonymous") @RequestParam final Boolean isAnonymous) { + feedbackService.setFeedbackAnonymousStatus(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..6cf88d741 --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/platform/feedback/Feedback.java @@ -0,0 +1,39 @@ +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.Data; +import lombok.NoArgsConstructor; + +/** Feedback for tracking member feedback. */ +@Data +@Builder(toBuilder = true) +@NoArgsConstructor +@AllArgsConstructor +public class Feedback { + + 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; + private Integer year; + private Boolean isAnonymous; + 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..b8e5eaa75 --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackDto.java @@ -0,0 +1,54 @@ +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; + 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..26250d125 --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackSearchCriteria.java @@ -0,0 +1,21 @@ +package com.wcc.platform.domain.platform.feedback; + +import com.wcc.platform.domain.platform.type.FeedbackType; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +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..01a1eca29 --- /dev/null +++ b/src/main/java/com/wcc/platform/domain/platform/feedback/validation/FeedbackValidator.java @@ -0,0 +1,56 @@ +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(); + + boolean isValid = true; + + // 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(); + isValid = false; + } + + // 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(); + isValid = false; + } + + // 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(); + isValid = false; + } + + return isValid; + } +} 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..3563ff555 --- /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; + } + } + return COMMUNITY_GENERAL; + } +} 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..67eaf1027 --- /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 setAnonymousStatus(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..287c4b67c --- /dev/null +++ b/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java @@ -0,0 +1,114 @@ +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.context.annotation.Primary; +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 +@Primary +@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 * 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 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 entity; + } + + @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 + public List getAll(final FeedbackSearchCriteria criteria) { + final StringBuilder sql = new StringBuilder("SELECT * FROM feedback 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 setAnonymousStatus(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..02e4491d1 --- /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.COLUMN_CREATED_AT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_FEEDBACK_TEXT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_FEEDBACK_TYPE_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_IS_ANONYMOUS; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_IS_APPROVED; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_MENTORSHIP_CYCLE_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_RATING; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWEE_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWER_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_UPDATED_AT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_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 +public class FeedbackMapper { + private 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 (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + private 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(COLUMN_ID)) + .reviewerId(rs.getLong(COLUMN_REVIEWER_ID)) + .reviewerName(null) + .revieweeId( + rs.getObject(COLUMN_REVIEWEE_ID) != null ? rs.getLong(COLUMN_REVIEWEE_ID) : null) + .revieweeName(null) + .mentorshipCycleId( + rs.getObject(COLUMN_MENTORSHIP_CYCLE_ID) != null + ? rs.getLong(COLUMN_MENTORSHIP_CYCLE_ID) + : null) + .feedbackType(FeedbackType.fromId(rs.getInt(COLUMN_FEEDBACK_TYPE_ID))) + .rating(rs.getObject(COLUMN_RATING) != null ? rs.getInt(COLUMN_RATING) : null) + .feedbackText(rs.getString(COLUMN_FEEDBACK_TEXT)) + .year(rs.getObject(COLUMN_YEAR) != null ? rs.getInt(COLUMN_YEAR) : null) + .isAnonymous(rs.getBoolean(COLUMN_IS_ANONYMOUS)) + .isApproved(rs.getBoolean(COLUMN_IS_APPROVED)) + .createdAt( + rs.getObject(COLUMN_CREATED_AT) != null + ? rs.getObject(COLUMN_CREATED_AT, OffsetDateTime.class) + : null) + .updatedAt( + rs.getObject(COLUMN_UPDATED_AT) != null + ? rs.getObject(COLUMN_UPDATED_AT, OffsetDateTime.class) + : null) + .build(); + } + + /** Adds a new feedback to the database and returns the feedback ID. */ + public Long addFeedback(final Feedback feedback) { + jdbc.update( + INSERT_SQL, + feedback.getReviewerId(), + feedback.getRevieweeId(), + feedback.getMentorshipCycleId(), + feedback.getFeedbackType().getTypeId(), + feedback.getRating(), + feedback.getFeedbackText(), + feedback.getYear(), + feedback.getIsAnonymous(), + feedback.getIsApproved()); + + // Return the last inserted ID + return jdbc.queryForObject("SELECT LASTVAL()", Long.class); + } + + /** + * 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..3327c9fab --- /dev/null +++ b/src/main/java/com/wcc/platform/repository/postgres/constants/FeedbackConstants.java @@ -0,0 +1,21 @@ +package com.wcc.platform.repository.postgres.constants; + +/** Constants related to Feedback entity. */ +public final class FeedbackConstants { + + public static final String TABLE = "feedback"; + public static final String COLUMN_ID = "id"; + public static final String COLUMN_REVIEWER_ID = "reviewer_id"; + public static final String COLUMN_REVIEWEE_ID = "reviewee_id"; + public static final String COLUMN_MENTORSHIP_CYCLE_ID = "mentorship_cycle_id"; + public static final String COLUMN_FEEDBACK_TYPE_ID = "feedback_type_id"; + public static final String COLUMN_RATING = "rating"; + public static final String COLUMN_FEEDBACK_TEXT = "feedback_text"; + public static final String COLUMN_YEAR = "feedback_year"; + public static final String COLUMN_IS_ANONYMOUS = "is_anonymous"; + public static final String COLUMN_IS_APPROVED = "is_approved"; + public static final String COLUMN_CREATED_AT = "created_at"; + public static final String COLUMN_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..4eedef6f2 --- /dev/null +++ b/src/main/java/com/wcc/platform/service/FeedbackService.java @@ -0,0 +1,166 @@ +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.domain.platform.member.Member; +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) { + final Member reviewer = validateReviewerExists(feedbackDto.getReviewerId()); + final Feedback feedback = feedbackDto.merge(); + + feedback.setReviewerName(reviewer.getFullName()); + + if (feedback.getRevieweeId() != null) { + final Member reviewee = validateRevieweeExists(feedback.getRevieweeId()); + feedback.setRevieweeName(reviewee.getFullName()); + } + + 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)); + + final Member reviewer = validateReviewerExists(feedbackDto.getReviewerId()); + + final Feedback updatedFeedback = feedbackDto.merge(); + + updatedFeedback.setId(feedbackId); + updatedFeedback.setIsApproved(existing.getIsApproved()); + updatedFeedback.setIsAnonymous(existing.getIsAnonymous()); + updatedFeedback.setReviewerName(reviewer.getFullName()); + + if (updatedFeedback.getRevieweeId() != null) { + final Member reviewee = validateRevieweeExists(updatedFeedback.getRevieweeId()); + updatedFeedback.setRevieweeName(reviewee.getFullName()); + } + + 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); + } + + /** Set anonymous status of feedback (to hide/show reviewer name). */ + public void setFeedbackAnonymousStatus(final Long feedbackId, final Boolean isAnonymous) { + feedbackRepository + .findById(feedbackId) + .orElseThrow(() -> new FeedbackNotFoundException(feedbackId)); + + feedbackRepository.setAnonymousStatus(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. + * + * @return Member + */ + private Member validateReviewerExists(final Long reviewerId) { + return memberRepository + .findById(reviewerId) + .orElseThrow( + () -> { + log.warn("Reviewer with ID {} not found", reviewerId); + return new MemberNotFoundException(reviewerId); + }); + } + + /** + * Validate that reviewee with given ID exists. + * + * @return Member + */ + private Member validateRevieweeExists(final Long revieweeId) { + return memberRepository + .findById(revieweeId) + .orElseThrow( + () -> { + log.warn("Reviewee with ID {} not found", revieweeId); + return new MemberNotFoundException(revieweeId); + }); + } +} diff --git a/src/main/resources/init-data/feedback.json b/src/main/resources/init-data/feedback.json new file mode 100644 index 000000000..bc0dba1ff --- /dev/null +++ b/src/main/resources/init-data/feedback.json @@ -0,0 +1,81 @@ +[ + { + "id": 1, + "reviewerId": 1, + "revieweeId": 2, + "mentorshipCycleId": 1, + "feedbackType": "MENTOR_REVIEW", + "rating": 5, + "feedbackText": "An exceptional mentor.", + "year": 2026, + "isAnonymous": false, + "isApproved": true, + "createdAt": "2026-01-15T10:30:00Z", + "updatedAt": "2026-01-15T10:30:00Z" + }, + { + "id": 2, + "reviewerId": 1, + "revieweeId": 2, + "mentorshipCycleId": 1, + "feedbackType": "MENTOR_REVIEW", + "rating": 5, + "feedbackText": "An exceptional mentor.", + "year": 2026, + "isAnonymous": true, + "isApproved": false, + "createdAt": "2026-01-20T14:15:00Z", + "updatedAt": "2026-01-20T14:15:00Z" + }, + { + "id": 3, + "reviewerId": 2, + "feedbackType": "COMMUNITY_GENERAL", + "rating": 5, + "feedbackText": "The Women Coding Community fantastic feedback.", + "year": 2026, + "isAnonymous": false, + "isApproved": true, + "createdAt": "2026-01-18T09:00:00Z", + "updatedAt": "2026-01-18T09:00:00Z" + }, + { + "id": 4, + "reviewerId": 2, + "feedbackType": "COMMUNITY_GENERAL", + "rating": 5, + "feedbackText": "The Women Coding Community fantastic feedback.", + "year": 2026, + "isAnonymous": true, + "isApproved": false, + "createdAt": "2026-02-05T16:45:00Z", + "updatedAt": "2026-02-05T16:45:00Z" + }, + { + "id": 5, + "reviewerId": 1, + "mentorshipCycleId": 1, + "feedbackType": "MENTORSHIP_PROGRAM", + "rating": 5, + "feedbackText": "The mentorship program feedback.", + "year": 2026, + "isAnonymous": true, + "isApproved": false, + "createdAt": "2026-02-10T11:20:00Z", + "updatedAt": "2026-02-10T11:20:00Z" + }, + { + "id": 6, + "reviewerId": 2, + "mentorshipCycleId": 1, + "feedbackType": "MENTORSHIP_PROGRAM", + "rating": 4, + "feedbackText": "The mentorship program feedback.", + "year": 2026, + "isAnonymous": false, + "isApproved": true, + "createdAt": "2026-02-12T13:30:00Z", + "updatedAt": "2026-02-12T13:30:00Z" + } +] + 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..3c4481b16 --- /dev/null +++ b/src/test/java/com/wcc/platform/controller/FeedbackControllerTest.java @@ -0,0 +1,433 @@ +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.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 + 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 + 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 + 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 + 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 + void testUpdateFeedbackReturnsOk() throws Exception { + Long feedbackId = 1L; + FeedbackDto feedbackDto = createMentorReviewFeedbackDtoTest(); + Feedback updatedFeedback = createMentorReviewFeedbackTest(); + updatedFeedback.setFeedbackText("Updated feedback text"); + updatedFeedback.setRating(4); + + 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 + 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 + void testApproveFeedbackReturnsOk() throws Exception { + Long feedbackId = 1L; + doNothing().when(feedbackService).approveFeedback(feedbackId); + + mockMvc + .perform( + MockMvcRequestBuilders.put(API_FEEDBACK + "/" + feedbackId + "/approve") + .header(API_KEY_HEADER, API_KEY_VALUE)) + .andExpect(status().isOk()); + + verify(feedbackService).approveFeedback(feedbackId); + } + + @Test + void testApproveNonExistentFeedbackThrowsException() throws Exception { + Long nonExistentFeedbackId = 999L; + doThrow(new FeedbackNotFoundException(nonExistentFeedbackId)) + .when(feedbackService) + .approveFeedback(nonExistentFeedbackId); + + mockMvc + .perform( + MockMvcRequestBuilders.put(API_FEEDBACK + "/" + nonExistentFeedbackId + "/approve") + .header(API_KEY_HEADER, API_KEY_VALUE)) + .andExpect(status().isNotFound()); + } + + @Test + void testSetFeedbackAnonymousStatusReturnsOk() throws Exception { + Long feedbackId = 1L; + Boolean isAnonymous = true; + doNothing().when(feedbackService).setFeedbackAnonymousStatus(feedbackId, isAnonymous); + + mockMvc + .perform( + MockMvcRequestBuilders.put(API_FEEDBACK + "/" + feedbackId + "/anonymous-status") + .header(API_KEY_HEADER, API_KEY_VALUE) + .param("isAnonymous", isAnonymous.toString())) + .andExpect(status().isOk()); + + verify(feedbackService).setFeedbackAnonymousStatus(feedbackId, isAnonymous); + } + + @Test + void testSetFeedbackAnonymousStatusToFalseReturnsOk() throws Exception { + Long feedbackId = 1L; + Boolean isAnonymous = false; + doNothing().when(feedbackService).setFeedbackAnonymousStatus(feedbackId, isAnonymous); + + mockMvc + .perform( + MockMvcRequestBuilders.put(API_FEEDBACK + "/" + feedbackId + "/anonymous-status") + .header(API_KEY_HEADER, API_KEY_VALUE) + .param("isAnonymous", isAnonymous.toString())) + .andExpect(status().isOk()); + + verify(feedbackService).setFeedbackAnonymousStatus(feedbackId, isAnonymous); + } + + @Test + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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/validation/FeedbackDtoValidationTest.java b/src/test/java/com/wcc/platform/domain/platform/feedback/validation/FeedbackDtoValidationTest.java new file mode 100644 index 000000000..8d6159973 --- /dev/null +++ b/src/test/java/com/wcc/platform/domain/platform/feedback/validation/FeedbackDtoValidationTest.java @@ -0,0 +1,235 @@ +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.Test; + +/** Test class for FeedbackDto validation. */ +class FeedbackDtoValidationTest { + + private Validator validator; + + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @Test + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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 + 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"))); + } +} 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..3de07c3ac --- /dev/null +++ b/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java @@ -0,0 +1,233 @@ +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.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. */ +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 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 + 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 + 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 + void testUpdate() { + Feedback feedback = createMentorReviewFeedbackTest(); + feedback.setFeedbackText("Updated feedback text"); + doNothing().when(feedbackMapper).updateFeedback(any(), anyLong()); + + Feedback result = repository.update(1L, feedback); + + assertNotNull(result); + assertEquals("Updated feedback text", result.getFeedbackText()); + verify(feedbackMapper).updateFeedback(feedback, 1L); + } + + @Test + 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 + 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 + void testDeleteById() { + Long feedbackId = 1L; + when(jdbc.update(DELETE_SQL, feedbackId)).thenReturn(1); + + repository.deleteById(feedbackId); + + verify(jdbc).update(DELETE_SQL, feedbackId); + } + + @Test + void testApproveFeedback() { + Long feedbackId = 1L; + when(jdbc.update(APPROVE_FEEDBACK, feedbackId)).thenReturn(1); + + repository.approveFeedback(feedbackId); + + verify(jdbc).update(APPROVE_FEEDBACK, feedbackId); + } + + @Test + void testSetAnonymousStatus() { + Long feedbackId = 1L; + Boolean isAnonymous = true; + when(jdbc.update(SET_ANONYMOUS_STATUS, isAnonymous, feedbackId)).thenReturn(1); + + repository.setAnonymousStatus(feedbackId, isAnonymous); + + verify(jdbc).update(SET_ANONYMOUS_STATUS, isAnonymous, feedbackId); + } + + @Test + void testSetAnonymousStatusToFalse() { + Long feedbackId = 1L; + Boolean isAnonymous = false; + when(jdbc.update(SET_ANONYMOUS_STATUS, isAnonymous, feedbackId)).thenReturn(1); + + repository.setAnonymousStatus(feedbackId, isAnonymous); + + verify(jdbc).update(SET_ANONYMOUS_STATUS, isAnonymous, feedbackId); + } + + @Test + 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 + void testDeleteByIdNonExistent() { + Long feedbackId = 999L; + when(jdbc.update(DELETE_SQL, feedbackId)).thenReturn(0); + + repository.deleteById(feedbackId); + + verify(jdbc).update(DELETE_SQL, feedbackId); + } + + @Test + void testGetAllNullCriteria() { + when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + .thenReturn(java.util.Collections.emptyList()); + + var result = repository.getAll(null); + + assertNotNull(result); + verify(jdbc) + .query(eq("SELECT * FROM feedback WHERE 1=1"), any(RowMapper.class), eq(new Object[0])); + } + + @Test + void testGetAllEmpty() { + FeedbackSearchCriteria criteria = FeedbackSearchCriteria.builder().build(); + when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + .thenReturn(java.util.Collections.emptyList()); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertTrue(result.isEmpty()); + verify(jdbc) + .query( + eq("SELECT * FROM feedback WHERE 1=1"), + any(org.springframework.jdbc.core.RowMapper.class), + eq(new Object[0])); + } + + @Test + 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(anyString(), any(RowMapper.class), any(Object[].class))) + .thenReturn(java.util.List.of(feedback1, feedback2)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(2, result.size()); + verify(jdbc) + .query( + eq( + "SELECT * FROM feedback WHERE 1=1 AND reviewer_id = ? AND reviewee_id = ? AND feedback_year = ?"), + any(RowMapper.class), + eq(new Object[] {reviewerId, revieweeId, year})); + } +} 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..b289979dc --- /dev/null +++ b/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java @@ -0,0 +1,292 @@ +package com.wcc.platform.repository.postgres.component; + +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_CREATED_AT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_FEEDBACK_TEXT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_FEEDBACK_TYPE_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_IS_ANONYMOUS; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_IS_APPROVED; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_MENTORSHIP_CYCLE_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_RATING; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWEE_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWER_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_UPDATED_AT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_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.ArgumentMatchers.eq; +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.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 + 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(COLUMN_ID)).thenReturn(feedbackId); + when(resultSet.getLong(COLUMN_REVIEWER_ID)).thenReturn(reviewerId); + when(resultSet.getObject(COLUMN_REVIEWEE_ID)).thenReturn(revieweeId); + when(resultSet.getLong(COLUMN_REVIEWEE_ID)).thenReturn(revieweeId); + when(resultSet.getObject(COLUMN_MENTORSHIP_CYCLE_ID)).thenReturn(mentorshipCycleId); + when(resultSet.getLong(COLUMN_MENTORSHIP_CYCLE_ID)).thenReturn(mentorshipCycleId); + when(resultSet.getInt(COLUMN_FEEDBACK_TYPE_ID)).thenReturn(feedbackTypeId); + when(resultSet.getObject(COLUMN_RATING)).thenReturn(rating); + when(resultSet.getInt(COLUMN_RATING)).thenReturn(rating); + when(resultSet.getString(COLUMN_FEEDBACK_TEXT)).thenReturn(feedbackText); + when(resultSet.getObject(COLUMN_YEAR)).thenReturn(year); + when(resultSet.getInt(COLUMN_YEAR)).thenReturn(year); + when(resultSet.getBoolean(COLUMN_IS_ANONYMOUS)).thenReturn(isAnonymous); + when(resultSet.getBoolean(COLUMN_IS_APPROVED)).thenReturn(isApproved); + when(resultSet.getObject(COLUMN_CREATED_AT)).thenReturn(createdAt); + when(resultSet.getObject(COLUMN_CREATED_AT, OffsetDateTime.class)).thenReturn(createdAt); + when(resultSet.getObject(COLUMN_UPDATED_AT)).thenReturn(updatedAt); + when(resultSet.getObject(COLUMN_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()); + assertNull(feedback.getReviewerName()); + assertNull(feedback.getRevieweeName()); + } + + @Test + 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(COLUMN_ID)).thenReturn(feedbackId); + when(resultSet.getLong(COLUMN_REVIEWER_ID)).thenReturn(reviewerId); + when(resultSet.getObject(COLUMN_REVIEWEE_ID)).thenReturn(null); + when(resultSet.getObject(COLUMN_MENTORSHIP_CYCLE_ID)).thenReturn(null); + when(resultSet.getInt(COLUMN_FEEDBACK_TYPE_ID)).thenReturn(feedbackTypeId); + when(resultSet.getObject(COLUMN_RATING)).thenReturn(null); + when(resultSet.getString(COLUMN_FEEDBACK_TEXT)).thenReturn(feedbackText); + when(resultSet.getObject(COLUMN_YEAR)).thenReturn(null); + when(resultSet.getBoolean(COLUMN_IS_ANONYMOUS)).thenReturn(isAnonymous); + when(resultSet.getBoolean(COLUMN_IS_APPROVED)).thenReturn(isApproved); + when(resultSet.getObject(COLUMN_CREATED_AT)).thenReturn(null); + when(resultSet.getObject(COLUMN_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 + void handlesSqlExceptionGracefully() throws Exception { + when(resultSet.getLong(COLUMN_ID)).thenThrow(SQLException.class); + assertThrows(SQLException.class, () -> feedbackMapper.mapRowToFeedback(resultSet)); + } + + @Test + 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(eq("SELECT LASTVAL()"), eq(Long.class))).thenReturn(100L); + + Long feedbackId = feedbackMapper.addFeedback(feedback); + + assertEquals(100L, feedbackId); + verify(jdbc) + .update( + eq( + "INSERT INTO feedback (" + + "reviewer_id, reviewee_id, mentorship_cycle_id, feedback_type_id, " + + "rating, feedback_text, feedback_year, is_anonymous, is_approved) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"), + eq(1L), + eq(2L), + eq(5L), + eq(1), + eq(5), + eq("Excellent mentor!"), + eq(2026), + eq(true), + eq(false)); + verify(jdbc).queryForObject(eq("SELECT LASTVAL()"), eq(Long.class)); + } + + @Test + void testAddFeedbackWithNullableFields() { + Feedback feedback = + Feedback.builder() + .reviewerId(1L) + .feedbackType(FeedbackType.COMMUNITY_GENERAL) + .feedbackText("Great community!") + .isAnonymous(false) + .isApproved(true) + .build(); + + when(jdbc.queryForObject(eq("SELECT LASTVAL()"), eq(Long.class))).thenReturn(200L); + + Long feedbackId = feedbackMapper.addFeedback(feedback); + + assertEquals(200L, feedbackId); + verify(jdbc) + .update( + eq( + "INSERT INTO feedback (" + + "reviewer_id, reviewee_id, mentorship_cycle_id, feedback_type_id, " + + "rating, feedback_text, feedback_year, is_anonymous, is_approved) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"), + eq(1L), + eq(null), + eq(null), + eq(2), + eq(null), + eq("Great community!"), + eq(null), + eq(false), + eq(true)); + } + + @Test + 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( + eq( + "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 = ?"), + eq(1L), + eq(2L), + eq(5L), + eq(1), + eq(4), + eq("Updated feedback text"), + eq(2026), + eq(false), + eq(true), + eq(feedbackId)); + } + + @Test + 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( + eq( + "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 = ?"), + eq(10L), + eq(null), + eq(null), + eq(3), + eq(null), + eq("Updated program feedback"), + eq(null), + eq(true), + eq(false), + eq(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..b040a775e --- /dev/null +++ b/src/test/java/com/wcc/platform/service/FeedbackServiceTest.java @@ -0,0 +1,334 @@ +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 testCreateCommunityGeneralFeedback_NoReviewee() { + 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 testCreateMentorshipProgramFeedback_Success() { + 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 testCreateFeedback_ReviewerNotFound() { + 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 testCreateFeedback_RevieweeNotFound() { + 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 testUpdateFeedback_NotFound() { + 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 testGetFeedbackById_NotFound() { + 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 testApproveFeedback_NotFound() { + 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 testSetFeedbackAnonymousStatus() { + final Long feedbackId = 1L; + when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.of(feedback)); + + service.setFeedbackAnonymousStatus(feedbackId, true); + + verify(feedbackRepository, times(1)).setAnonymousStatus(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 testDeleteFeedback_NotFound() { + 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(); + assertThat(result).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(); + assertThat(result).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(); + assertThat(result).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()); + } +} From d088391476889944450c3d0547e6d27248fccd8c Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Tue, 17 Mar 2026 11:13:59 +0100 Subject: [PATCH 02/17] Feedback API Add feedback migration file. --- .../V31__20260118__create_feedback_system.sql | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/main/resources/db/migration/V31__20260118__create_feedback_system.sql diff --git a/src/main/resources/db/migration/V31__20260118__create_feedback_system.sql b/src/main/resources/db/migration/V31__20260118__create_feedback_system.sql new file mode 100644 index 000000000..5a8a02aba --- /dev/null +++ b/src/main/resources/db/migration/V31__20260118__create_feedback_system.sql @@ -0,0 +1,59 @@ +-- 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; + +-- Mentorship cycle table +CREATE TABLE IF NOT EXISTS mentorship_cycle +( + id SERIAL PRIMARY KEY, + cycle_type INTEGER NOT NULL REFERENCES mentorship_types (id), + month_num INTEGER, + description TEXT, + start_date DATE, + end_date DATE, + is_active BOOLEAN DEFAULT TRUE, + created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP +); + +-- 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_cycle (id) ON DELETE SET NULL, + feedback_type_id INTEGER NOT NULL REFERENCES feedback_types (id), + rating INTEGER CHECK (rating >= 0 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 + ) +); + +-- 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); + From 409fb56b72a48d51fe6e7bb40a9f0a84b1b7a2fc Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Sun, 22 Mar 2026 14:49:07 +0100 Subject: [PATCH 03/17] Feedback API Update migration file name. --- ...dback_system.sql => V34__20260118__create_feedback_system.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V31__20260118__create_feedback_system.sql => V34__20260118__create_feedback_system.sql} (100%) diff --git a/src/main/resources/db/migration/V31__20260118__create_feedback_system.sql b/src/main/resources/db/migration/V34__20260118__create_feedback_system.sql similarity index 100% rename from src/main/resources/db/migration/V31__20260118__create_feedback_system.sql rename to src/main/resources/db/migration/V34__20260118__create_feedback_system.sql From 8f4a5ab5c0912713e4035ae9de24b0041724edb3 Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Sun, 22 Mar 2026 22:35:35 +0100 Subject: [PATCH 04/17] Feedback API Update/add mentor.json, feedback.json, mentee.json --- src/main/resources/init-data/feedback.json | 96 ++++++------ src/main/resources/init-data/mentee.json | 164 +++++++++++++++++++++ src/main/resources/init-data/mentor.json | 38 ++++- 3 files changed, 248 insertions(+), 50 deletions(-) create mode 100644 src/main/resources/init-data/mentee.json diff --git a/src/main/resources/init-data/feedback.json b/src/main/resources/init-data/feedback.json index bc0dba1ff..d9775dd8f 100644 --- a/src/main/resources/init-data/feedback.json +++ b/src/main/resources/init-data/feedback.json @@ -1,81 +1,91 @@ [ { - "id": 1, "reviewerId": 1, "revieweeId": 2, "mentorshipCycleId": 1, "feedbackType": "MENTOR_REVIEW", "rating": 5, - "feedbackText": "An exceptional mentor.", + "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, - "isApproved": true, - "createdAt": "2026-01-15T10:30:00Z", - "updatedAt": "2026-01-15T10:30:00Z" + "isAnonymous": false }, { - "id": 2, - "reviewerId": 1, - "revieweeId": 2, + "reviewerId": 2, + "revieweeId": 3, "mentorshipCycleId": 1, "feedbackType": "MENTOR_REVIEW", - "rating": 5, - "feedbackText": "An exceptional mentor.", + "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, - "isApproved": false, - "createdAt": "2026-01-20T14:15:00Z", - "updatedAt": "2026-01-20T14:15:00Z" + "isAnonymous": true }, { - "id": 3, - "reviewerId": 2, + "reviewerId": 3, "feedbackType": "COMMUNITY_GENERAL", - "rating": 5, - "feedbackText": "The Women Coding Community fantastic feedback.", + "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, - "isApproved": true, - "createdAt": "2026-01-18T09:00:00Z", - "updatedAt": "2026-01-18T09:00:00Z" + "isAnonymous": false }, { - "id": 4, - "reviewerId": 2, + "reviewerId": 4, "feedbackType": "COMMUNITY_GENERAL", - "rating": 5, - "feedbackText": "The Women Coding Community fantastic feedback.", + "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, - "isApproved": false, - "createdAt": "2026-02-05T16:45:00Z", - "updatedAt": "2026-02-05T16:45:00Z" + "isAnonymous": true }, { - "id": 5, "reviewerId": 1, "mentorshipCycleId": 1, "feedbackType": "MENTORSHIP_PROGRAM", "rating": 5, - "feedbackText": "The mentorship program feedback.", + "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": true, - "isApproved": false, - "createdAt": "2026-02-10T11:20:00Z", - "updatedAt": "2026-02-10T11:20:00Z" + "isAnonymous": false }, { - "id": 6, "reviewerId": 2, "mentorshipCycleId": 1, "feedbackType": "MENTORSHIP_PROGRAM", "rating": 4, - "feedbackText": "The mentorship program feedback.", + "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, - "isApproved": true, - "createdAt": "2026-02-12T13:30:00Z", - "updatedAt": "2026-02-12T13:30:00Z" + "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 c6d02ff25..5e17a3393 100644 --- a/src/main/resources/init-data/mentor.json +++ b/src/main/resources/init-data/mentor.json @@ -20,18 +20,39 @@ "link": "https://www.linkedin.com/in/dricazenck/" } ], + "pronouns": "she/her", + "pronounCategory": "FEMININE", + "isWomen": true, "profileStatus": "ACTIVE", "skills": { "yearsExperience": 15, "areas": [ - "BACKEND", - "FULLSTACK", - "DISTRIBUTED_SYSTEMS" + { + "technicalArea": "BACKEND", + "proficiencyLevel": "EXPERT" + }, + { + "technicalArea": "FULLSTACK", + "proficiencyLevel": "ADVANCED" + }, + { + "technicalArea": "DISTRIBUTED_SYSTEMS", + "proficiencyLevel": "ADVANCED" + } ], "languages": [ - "java", - "javascript", - "Kotlin" + { + "language": "JAVA", + "proficiencyLevel": "EXPERT" + }, + { + "language": "JAVASCRIPT", + "proficiencyLevel": "ADVANCED" + }, + { + "language": "KOTLIN", + "proficiencyLevel": "INTERMEDIATE" + } ], "mentorshipFocus": [ "Grow from beginner to mid-level", @@ -77,5 +98,8 @@ "hours": 2 } ] - } + }, + "calendlyLink": "https://calendly.com/adriana-zencke", + "acceptMale": true, + "acceptPromotion": true } From de1fee3aebf441e2d16a042618f65f7446a305f4 Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Mon, 6 Apr 2026 12:57:13 +0200 Subject: [PATCH 05/17] Feedback API Update Feedback.java and FeedbackSearchCriteria to use only needed lombok annotations Update FeedbackController.java - use Patch instead of Put for approveFeedback and setFeedbackAnonymousStatus Update/Add tests Feedback coverage is above 70% threshold. Implement PMD suggestions. --- .../configuration/GlobalExceptionHandler.java | 1 + .../controller/FeedbackController.java | 11 +- .../domain/platform/feedback/Feedback.java | 21 +- .../feedback/FeedbackSearchCriteria.java | 10 +- .../validation/FeedbackValidator.java | 23 +- .../repository/FeedbackRepository.java | 2 +- .../postgres/PostgresFeedbackRepository.java | 3 +- .../postgres/component/FeedbackMapper.java | 1 + .../postgres/constants/FeedbackConstants.java | 1 + .../wcc/platform/service/FeedbackService.java | 6 +- ...V35__20260118__create_feedback_system.sql} | 0 .../controller/FeedbackControllerTest.java | 28 +- .../feedback/FeedbackSearchCriteriaTest.java | 231 +++++++++++++ .../platform/feedback/FeedbackTest.java | 285 ++++++++++++++++ .../PostgresFeedbackRepositoryTest.java | 307 +++++++++++++++++- .../platform/service/FeedbackServiceTest.java | 22 +- 16 files changed, 891 insertions(+), 61 deletions(-) rename src/main/resources/db/migration/{V34__20260118__create_feedback_system.sql => V35__20260118__create_feedback_system.sql} (100%) create mode 100644 src/test/java/com/wcc/platform/domain/platform/feedback/FeedbackSearchCriteriaTest.java create mode 100644 src/test/java/com/wcc/platform/domain/platform/feedback/FeedbackTest.java diff --git a/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java b/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java index a21804472..b5bfde800 100644 --- a/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java +++ b/src/main/java/com/wcc/platform/configuration/GlobalExceptionHandler.java @@ -34,6 +34,7 @@ /** Global controller to handle all exceptions for the API. */ @RestControllerAdvice +@SuppressWarnings("PMD.ExcessiveImports") public class GlobalExceptionHandler { /** Receive ContentNotFoundException and return {@link HttpStatus#NOT_FOUND}. */ diff --git a/src/main/java/com/wcc/platform/controller/FeedbackController.java b/src/main/java/com/wcc/platform/controller/FeedbackController.java index 5bdd90e7f..1a7c522ad 100644 --- a/src/main/java/com/wcc/platform/controller/FeedbackController.java +++ b/src/main/java/com/wcc/platform/controller/FeedbackController.java @@ -15,6 +15,7 @@ import org.springframework.http.ResponseEntity; 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; @@ -128,7 +129,7 @@ public ResponseEntity updateFeedback( * @param feedbackId ID of the feedback to approve * @return No content */ - @PutMapping("/{feedbackId}/approve") + @PatchMapping("/{feedbackId}/approve") @Operation(summary = "Approve feedback (admin only)") @ResponseStatus(HttpStatus.OK) public ResponseEntity approveFeedback( @@ -144,13 +145,13 @@ public ResponseEntity approveFeedback( * @param isAnonymous true to hide reviewer name, false to show reviewer name * @return No content */ - @PutMapping("/{feedbackId}/anonymous-status") - @Operation(summary = "Set feedback anonymous status (to hide/show reviewer name)") + @PatchMapping("/{feedbackId}/anonymous-status") + @Operation(summary = "Update feedback anonymous status (to hide/show reviewer name)") @ResponseStatus(HttpStatus.OK) - public ResponseEntity setFeedbackAnonymousStatus( + public ResponseEntity updateFeedbackAnonymousStatus( @Parameter(description = "ID of the feedback") @PathVariable final Long feedbackId, @Parameter(description = "Is anonymous") @RequestParam final Boolean isAnonymous) { - feedbackService.setFeedbackAnonymousStatus(feedbackId, isAnonymous); + feedbackService.updateFeedbackAnonymousStatus(feedbackId, isAnonymous); return ResponseEntity.ok().build(); } 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 index 6cf88d741..c3a25dfca 100644 --- a/src/main/java/com/wcc/platform/domain/platform/feedback/Feedback.java +++ b/src/main/java/com/wcc/platform/domain/platform/feedback/Feedback.java @@ -8,21 +8,26 @@ import java.time.OffsetDateTime; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; +import lombok.ToString; /** Feedback for tracking member feedback. */ -@Data -@Builder(toBuilder = true) @NoArgsConstructor @AllArgsConstructor +@ToString +@EqualsAndHashCode +@Getter +@Builder(toBuilder = true) public class Feedback { - private Long id; + @Setter private Long id; @NotNull private Long reviewerId; - private String reviewerName; + @Setter private String reviewerName; private Long revieweeId; // For MENTOR_REVIEW - private String revieweeName; + @Setter private String revieweeName; private Long mentorshipCycleId; // For MENTORSHIP_PROGRAM @NotNull private FeedbackType feedbackType; @@ -32,8 +37,8 @@ public class Feedback { @NotBlank private String feedbackText; private Integer year; - private Boolean isAnonymous; - private Boolean isApproved; + @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/FeedbackSearchCriteria.java b/src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackSearchCriteria.java index 26250d125..fa774debe 100644 --- a/src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackSearchCriteria.java +++ b/src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackSearchCriteria.java @@ -3,13 +3,17 @@ import com.wcc.platform.domain.platform.type.FeedbackType; import lombok.AllArgsConstructor; import lombok.Builder; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.ToString; -@Data -@Builder @NoArgsConstructor @AllArgsConstructor +@ToString +@EqualsAndHashCode +@Getter +@Builder public class FeedbackSearchCriteria { private Long reviewerId; private Long revieweeId; 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 index 01a1eca29..5988d9559 100644 --- 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 @@ -16,8 +16,13 @@ public boolean isValid(final FeedbackDto dto, final ConstraintValidatorContext c context.disableDefaultConstraintViolation(); - boolean isValid = true; + 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) @@ -27,18 +32,25 @@ public boolean isValid(final FeedbackDto dto, final ConstraintValidatorContext c "mentorshipCycleId is required for " + dto.getFeedbackType() + " feedback") .addPropertyNode("mentorshipCycleId") .addConstraintViolation(); - isValid = false; + 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(); - isValid = false; + 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) @@ -48,9 +60,8 @@ public boolean isValid(final FeedbackDto dto, final ConstraintValidatorContext c "rating is required for " + dto.getFeedbackType() + " feedback") .addPropertyNode("rating") .addConstraintViolation(); - isValid = false; + return false; } - - return isValid; + return true; } } diff --git a/src/main/java/com/wcc/platform/repository/FeedbackRepository.java b/src/main/java/com/wcc/platform/repository/FeedbackRepository.java index 67eaf1027..4d9c2e2cc 100644 --- a/src/main/java/com/wcc/platform/repository/FeedbackRepository.java +++ b/src/main/java/com/wcc/platform/repository/FeedbackRepository.java @@ -31,5 +31,5 @@ public interface FeedbackRepository extends CrudRepository { * @param feedbackId the ID of the feedback * @param isAnonymous true to hide reviewer name, false to show reviewer name */ - void setAnonymousStatus(Long feedbackId, Boolean isAnonymous); + 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 index 287c4b67c..c0b3310c0 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java +++ b/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java @@ -63,6 +63,7 @@ public void deleteById(final Long feedbackId) { } @Override + @SuppressWarnings({"PMD.InsufficientStringBufferDeclaration", "PMD.CognitiveComplexity"}) public List getAll(final FeedbackSearchCriteria criteria) { final StringBuilder sql = new StringBuilder("SELECT * FROM feedback WHERE 1=1"); final List params = new ArrayList<>(); @@ -108,7 +109,7 @@ public void approveFeedback(final Long feedbackId) { } @Override - public void setAnonymousStatus(final Long feedbackId, final Boolean isAnonymous) { + 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 index 02e4491d1..72e813824 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/component/FeedbackMapper.java +++ b/src/main/java/com/wcc/platform/repository/postgres/component/FeedbackMapper.java @@ -25,6 +25,7 @@ /** Maps database result sets to Feedback domain objects. */ @Component @RequiredArgsConstructor +@SuppressWarnings("PMD.TooManyStaticImports") public class FeedbackMapper { private static final String INSERT_SQL = "INSERT INTO feedback (" 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 index 3327c9fab..b67c28b5e 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/constants/FeedbackConstants.java +++ b/src/main/java/com/wcc/platform/repository/postgres/constants/FeedbackConstants.java @@ -1,6 +1,7 @@ package com.wcc.platform.repository.postgres.constants; /** Constants related to Feedback entity. */ +@SuppressWarnings({"PMD.DataClass", "PMD.LongVariable"}) public final class FeedbackConstants { public static final String TABLE = "feedback"; diff --git a/src/main/java/com/wcc/platform/service/FeedbackService.java b/src/main/java/com/wcc/platform/service/FeedbackService.java index 4eedef6f2..8a9463db0 100644 --- a/src/main/java/com/wcc/platform/service/FeedbackService.java +++ b/src/main/java/com/wcc/platform/service/FeedbackService.java @@ -99,13 +99,13 @@ public void approveFeedback(final Long feedbackId) { feedbackRepository.approveFeedback(feedbackId); } - /** Set anonymous status of feedback (to hide/show reviewer name). */ - public void setFeedbackAnonymousStatus(final Long feedbackId, final Boolean isAnonymous) { + /** 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.setAnonymousStatus(feedbackId, isAnonymous); + feedbackRepository.updateAnonymousStatus(feedbackId, isAnonymous); } /** Delete feedback by ID. */ diff --git a/src/main/resources/db/migration/V34__20260118__create_feedback_system.sql b/src/main/resources/db/migration/V35__20260118__create_feedback_system.sql similarity index 100% rename from src/main/resources/db/migration/V34__20260118__create_feedback_system.sql rename to src/main/resources/db/migration/V35__20260118__create_feedback_system.sql diff --git a/src/test/java/com/wcc/platform/controller/FeedbackControllerTest.java b/src/test/java/com/wcc/platform/controller/FeedbackControllerTest.java index 3c4481b16..d68262024 100644 --- a/src/test/java/com/wcc/platform/controller/FeedbackControllerTest.java +++ b/src/test/java/com/wcc/platform/controller/FeedbackControllerTest.java @@ -117,9 +117,11 @@ void testCreateCommunityFeedbackReturnsCreated() throws Exception { void testUpdateFeedbackReturnsOk() throws Exception { Long feedbackId = 1L; FeedbackDto feedbackDto = createMentorReviewFeedbackDtoTest(); - Feedback updatedFeedback = createMentorReviewFeedbackTest(); - updatedFeedback.setFeedbackText("Updated feedback text"); - updatedFeedback.setRating(4); + Feedback updatedFeedback = + createMentorReviewFeedbackTest().toBuilder() + .feedbackText("Updated feedback text") + .rating(4) + .build(); when(feedbackService.updateFeedback(eq(feedbackId), any(FeedbackDto.class))) .thenReturn(updatedFeedback); @@ -160,7 +162,7 @@ void testApproveFeedbackReturnsOk() throws Exception { mockMvc .perform( - MockMvcRequestBuilders.put(API_FEEDBACK + "/" + feedbackId + "/approve") + MockMvcRequestBuilders.patch(API_FEEDBACK + "/" + feedbackId + "/approve") .header(API_KEY_HEADER, API_KEY_VALUE)) .andExpect(status().isOk()); @@ -176,41 +178,41 @@ void testApproveNonExistentFeedbackThrowsException() throws Exception { mockMvc .perform( - MockMvcRequestBuilders.put(API_FEEDBACK + "/" + nonExistentFeedbackId + "/approve") + MockMvcRequestBuilders.patch(API_FEEDBACK + "/" + nonExistentFeedbackId + "/approve") .header(API_KEY_HEADER, API_KEY_VALUE)) .andExpect(status().isNotFound()); } @Test - void testSetFeedbackAnonymousStatusReturnsOk() throws Exception { + void testUpdateFeedbackAnonymousStatusReturnsOk() throws Exception { Long feedbackId = 1L; Boolean isAnonymous = true; - doNothing().when(feedbackService).setFeedbackAnonymousStatus(feedbackId, isAnonymous); + doNothing().when(feedbackService).updateFeedbackAnonymousStatus(feedbackId, isAnonymous); mockMvc .perform( - MockMvcRequestBuilders.put(API_FEEDBACK + "/" + feedbackId + "/anonymous-status") + MockMvcRequestBuilders.patch(API_FEEDBACK + "/" + feedbackId + "/anonymous-status") .header(API_KEY_HEADER, API_KEY_VALUE) .param("isAnonymous", isAnonymous.toString())) .andExpect(status().isOk()); - verify(feedbackService).setFeedbackAnonymousStatus(feedbackId, isAnonymous); + verify(feedbackService).updateFeedbackAnonymousStatus(feedbackId, isAnonymous); } @Test - void testSetFeedbackAnonymousStatusToFalseReturnsOk() throws Exception { + void testUpdateFeedbackAnonymousStatusToFalseReturnsOk() throws Exception { Long feedbackId = 1L; Boolean isAnonymous = false; - doNothing().when(feedbackService).setFeedbackAnonymousStatus(feedbackId, isAnonymous); + doNothing().when(feedbackService).updateFeedbackAnonymousStatus(feedbackId, isAnonymous); mockMvc .perform( - MockMvcRequestBuilders.put(API_FEEDBACK + "/" + feedbackId + "/anonymous-status") + MockMvcRequestBuilders.patch(API_FEEDBACK + "/" + feedbackId + "/anonymous-status") .header(API_KEY_HEADER, API_KEY_VALUE) .param("isAnonymous", isAnonymous.toString())) .andExpect(status().isOk()); - verify(feedbackService).setFeedbackAnonymousStatus(feedbackId, isAnonymous); + verify(feedbackService).updateFeedbackAnonymousStatus(feedbackId, isAnonymous); } @Test 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..58cf10925 --- /dev/null +++ b/src/test/java/com/wcc/platform/domain/platform/feedback/FeedbackTest.java @@ -0,0 +1,285 @@ +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.setReviewerName("New Reviewer"); + feedback.setRevieweeName("New Reviewee"); + feedback.setIsAnonymous(false); + feedback.setIsApproved(true); + + assertEquals(99L, feedback.getId()); + assertEquals("New Reviewer", feedback.getReviewerName()); + assertEquals("New Reviewee", feedback.getRevieweeName()); + 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); + } + + @Test + @DisplayName("Given same feedback instance, when equals called, then returns true") + void testEqualsSameInstance() { + assertEquals(feedback, 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 index 3de07c3ac..0d878bc73 100644 --- a/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java +++ b/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java @@ -28,6 +28,7 @@ import org.springframework.jdbc.core.RowMapper; /** PostgresFeedbackRepositoryTest class for testing the PostgresFeedbackRepository. */ +@SuppressWarnings("PMD.TooManyMethods") class PostgresFeedbackRepositoryTest { private static final String DELETE_SQL = "DELETE FROM feedback WHERE id = ?"; @@ -72,8 +73,8 @@ void testCreateThrowsWhenFindByIdEmpty() { @Test void testUpdate() { - Feedback feedback = createMentorReviewFeedbackTest(); - feedback.setFeedbackText("Updated feedback text"); + Feedback feedback = + createMentorReviewFeedbackTest().toBuilder().feedbackText("Updated feedback text").build(); doNothing().when(feedbackMapper).updateFeedback(any(), anyLong()); Feedback result = repository.update(1L, feedback); @@ -131,23 +132,23 @@ void testApproveFeedback() { } @Test - void testSetAnonymousStatus() { + void testUpdateAnonymousStatus() { Long feedbackId = 1L; Boolean isAnonymous = true; when(jdbc.update(SET_ANONYMOUS_STATUS, isAnonymous, feedbackId)).thenReturn(1); - repository.setAnonymousStatus(feedbackId, isAnonymous); + repository.updateAnonymousStatus(feedbackId, isAnonymous); verify(jdbc).update(SET_ANONYMOUS_STATUS, isAnonymous, feedbackId); } @Test - void testSetAnonymousStatusToFalse() { + void testUpdateAnonymousStatusToFalse() { Long feedbackId = 1L; Boolean isAnonymous = false; when(jdbc.update(SET_ANONYMOUS_STATUS, isAnonymous, feedbackId)).thenReturn(1); - repository.setAnonymousStatus(feedbackId, isAnonymous); + repository.updateAnonymousStatus(feedbackId, isAnonymous); verify(jdbc).update(SET_ANONYMOUS_STATUS, isAnonymous, feedbackId); } @@ -161,6 +162,19 @@ void testFindByIdJdbcThrows() { assertThrows(RuntimeException.class, () -> repository.findById(feedbackId)); } + @Test + void testGetAllThrowsFeedbackNotFoundException() { + FeedbackSearchCriteria criteria = FeedbackSearchCriteria.builder().reviewerId(1L).build(); + when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + .thenThrow( + new com.wcc.platform.domain.exceptions.FeedbackNotFoundException( + "Invalid search criteria")); + + assertThrows( + com.wcc.platform.domain.exceptions.FeedbackNotFoundException.class, + () -> repository.getAll(criteria)); + } + @Test void testDeleteByIdNonExistent() { Long feedbackId = 999L; @@ -194,10 +208,7 @@ void testGetAllEmpty() { assertNotNull(result); assertTrue(result.isEmpty()); verify(jdbc) - .query( - eq("SELECT * FROM feedback WHERE 1=1"), - any(org.springframework.jdbc.core.RowMapper.class), - eq(new Object[0])); + .query(eq("SELECT * FROM feedback WHERE 1=1"), any(RowMapper.class), eq(new Object[0])); } @Test @@ -230,4 +241,280 @@ void testGetAllWithMultipleCriteria() { any(RowMapper.class), eq(new Object[] {reviewerId, revieweeId, year})); } + + @Test + void testGetAllWithReviewerIdOnly() { + Long reviewerId = 1L; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().reviewerId(reviewerId).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq("SELECT * FROM feedback WHERE 1=1 AND reviewer_id = ?"), + any(RowMapper.class), + eq(new Object[] {reviewerId})); + } + + @Test + void testGetAllWithRevieweeIdOnly() { + Long revieweeId = 2L; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().revieweeId(revieweeId).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq("SELECT * FROM feedback WHERE 1=1 AND reviewee_id = ?"), + any(RowMapper.class), + eq(new Object[] {revieweeId})); + } + + @Test + 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(Object[].class))) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq("SELECT * FROM feedback WHERE 1=1 AND feedback_type_id = ?"), + any(RowMapper.class), + eq(new Object[] {1})); + } + + @Test + void testGetAllWithYearOnly() { + Integer year = 2026; + FeedbackSearchCriteria criteria = FeedbackSearchCriteria.builder().year(year).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq("SELECT * FROM feedback WHERE 1=1 AND feedback_year = ?"), + any(RowMapper.class), + eq(new Object[] {year})); + } + + @Test + void testGetAllWithMentorshipCycleIdOnly() { + Long mentorshipCycleId = 5L; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().mentorshipCycleId(mentorshipCycleId).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq("SELECT * FROM feedback WHERE 1=1 AND mentorship_cycle_id = ?"), + any(RowMapper.class), + eq(new Object[] {mentorshipCycleId})); + } + + @Test + void testGetAllWithIsApprovedTrue() { + Boolean isApproved = true; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().isApproved(isApproved).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq("SELECT * FROM feedback WHERE 1=1 AND is_approved = ?"), + any(RowMapper.class), + eq(new Object[] {isApproved})); + } + + @Test + void testGetAllWithIsApprovedFalse() { + Boolean isApproved = false; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().isApproved(isApproved).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq("SELECT * FROM feedback WHERE 1=1 AND is_approved = ?"), + any(RowMapper.class), + eq(new Object[] {isApproved})); + } + + @Test + void testGetAllWithIsAnonymousTrue() { + Boolean isAnonymous = true; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().isAnonymous(isAnonymous).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq("SELECT * FROM feedback WHERE 1=1 AND is_anonymous = ?"), + any(RowMapper.class), + eq(new Object[] {isAnonymous})); + } + + @Test + void testGetAllWithIsAnonymousFalse() { + Boolean isAnonymous = false; + FeedbackSearchCriteria criteria = + FeedbackSearchCriteria.builder().isAnonymous(isAnonymous).build(); + + Feedback feedback = createMentorReviewFeedbackTest(); + when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq("SELECT * FROM feedback WHERE 1=1 AND is_anonymous = ?"), + any(RowMapper.class), + eq(new Object[] {isAnonymous})); + } + + @Test + 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(anyString(), any(RowMapper.class), any(Object[].class))) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + assertEquals(1, result.size()); + verify(jdbc) + .query( + eq( + "SELECT * FROM feedback WHERE 1=1 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 + 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(Object[].class))) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + verify(jdbc) + .query( + eq("SELECT * FROM feedback WHERE 1=1 AND feedback_type_id = ? AND feedback_year = ?"), + any(RowMapper.class), + eq(new Object[] {2, year})); + } + + @Test + 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(Object[].class))) + .thenReturn(java.util.List.of(feedback)); + + var result = repository.getAll(criteria); + + assertNotNull(result); + verify(jdbc) + .query( + eq("SELECT * FROM feedback WHERE 1=1 AND mentorship_cycle_id = ? AND is_approved = ?"), + any(RowMapper.class), + eq(new Object[] {mentorshipCycleId, isApproved})); + } } diff --git a/src/test/java/com/wcc/platform/service/FeedbackServiceTest.java b/src/test/java/com/wcc/platform/service/FeedbackServiceTest.java index b040a775e..bd090aba4 100644 --- a/src/test/java/com/wcc/platform/service/FeedbackServiceTest.java +++ b/src/test/java/com/wcc/platform/service/FeedbackServiceTest.java @@ -83,7 +83,7 @@ void testCreateFeedback() { @Test @DisplayName("Should create community general feedback without reviewee") - void testCreateCommunityGeneralFeedback_NoReviewee() { + void testCreateCommunityGeneralFeedbackNoReviewee() { when(memberRepository.findById(communityFeedbackDto.getReviewerId())) .thenReturn(Optional.of(reviewer)); when(feedbackRepository.create(any(Feedback.class))).thenReturn(communityFeedback); @@ -98,7 +98,7 @@ void testCreateCommunityGeneralFeedback_NoReviewee() { @Test @DisplayName("Should create mentorship program feedback") - void testCreateMentorshipProgramFeedback_Success() { + void testCreateMentorshipProgramFeedbackSuccess() { when(memberRepository.findById(mentorshipFeedbackDto.getReviewerId())) .thenReturn(Optional.of(reviewer)); when(feedbackRepository.create(any(Feedback.class))).thenReturn(mentorshipFeedback); @@ -114,7 +114,7 @@ void testCreateMentorshipProgramFeedback_Success() { @Test @DisplayName("Should throw MemberNotFoundException when reviewer not found") - void testCreateFeedback_ReviewerNotFound() { + void testCreateFeedbackReviewerNotFound() { when(memberRepository.findById(feedbackDto.getReviewerId())).thenReturn(Optional.empty()); assertThrows(MemberNotFoundException.class, () -> service.createFeedback(feedbackDto)); @@ -123,7 +123,7 @@ void testCreateFeedback_ReviewerNotFound() { @Test @DisplayName("Should throw MemberNotFoundException when reviewee not found") - void testCreateFeedback_RevieweeNotFound() { + void testCreateFeedbackRevieweeNotFound() { when(memberRepository.findById(feedbackDto.getReviewerId())).thenReturn(Optional.of(reviewer)); when(memberRepository.findById(feedbackDto.getRevieweeId())).thenReturn(Optional.empty()); @@ -151,7 +151,7 @@ void testUpdateFeedback() { @Test @DisplayName("Should throw FeedbackNotFoundException when updating non-existent feedback") - void testUpdateFeedback_NotFound() { + void testUpdateFeedbackNotFound() { final Long feedbackId = 999L; when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.empty()); @@ -174,7 +174,7 @@ void testGetFeedbackById() { @Test @DisplayName("Should throw FeedbackNotFoundException when getting non-existent feedback") - void testGetFeedbackById_NotFound() { + void testGetFeedbackByIdNotFound() { final Long feedbackId = 999L; when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.empty()); @@ -194,7 +194,7 @@ void testApproveFeedback() { @Test @DisplayName("Should throw FeedbackNotFoundException when approving non-existent feedback") - void testApproveFeedback_NotFound() { + void testApproveFeedbackNotFound() { final Long feedbackId = 999L; when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.empty()); @@ -204,13 +204,13 @@ void testApproveFeedback_NotFound() { @Test @DisplayName("Should set feedback anonymous status successfully") - void testSetFeedbackAnonymousStatus() { + void testUpdateFeedbackAnonymousStatus() { final Long feedbackId = 1L; when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.of(feedback)); - service.setFeedbackAnonymousStatus(feedbackId, true); + service.updateFeedbackAnonymousStatus(feedbackId, true); - verify(feedbackRepository, times(1)).setAnonymousStatus(feedbackId, true); + verify(feedbackRepository, times(1)).updateAnonymousStatus(feedbackId, true); } @Test @@ -226,7 +226,7 @@ void testDeleteFeedback() { @Test @DisplayName("Should throw FeedbackNotFoundException when deleting non-existent feedback") - void testDeleteFeedback_NotFound() { + void testDeleteFeedbackNotFound() { final Long feedbackId = 999L; when(feedbackRepository.findById(feedbackId)).thenReturn(Optional.empty()); From c8cb6765f82e9eadeaa1e8adab5c1b48951d0eff Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Mon, 6 Apr 2026 20:55:15 +0200 Subject: [PATCH 06/17] Feedback API "chore: rename feedback migration to V36 to maintain chronological order after main merge" --- ...dback_system.sql => V36__20260406__create_feedback_system.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/main/resources/db/migration/{V35__20260118__create_feedback_system.sql => V36__20260406__create_feedback_system.sql} (100%) diff --git a/src/main/resources/db/migration/V35__20260118__create_feedback_system.sql b/src/main/resources/db/migration/V36__20260406__create_feedback_system.sql similarity index 100% rename from src/main/resources/db/migration/V35__20260118__create_feedback_system.sql rename to src/main/resources/db/migration/V36__20260406__create_feedback_system.sql From 144af13b36230ac6a21679a2f04c2744dd1a3f9f Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Tue, 7 Apr 2026 21:55:02 +0200 Subject: [PATCH 07/17] Feedback API Fix SonarQube issues. --- .../component/FeedbackMapperTest.java | 127 +++++++++--------- .../platform/service/FeedbackServiceTest.java | 9 +- 2 files changed, 64 insertions(+), 72 deletions(-) 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 index b289979dc..ab4d68cc8 100644 --- a/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java +++ b/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java @@ -16,7 +16,6 @@ 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.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -158,28 +157,27 @@ void testAddFeedback() { .isApproved(false) .build(); - when(jdbc.queryForObject(eq("SELECT LASTVAL()"), eq(Long.class))).thenReturn(100L); + when(jdbc.queryForObject("SELECT LASTVAL()", Long.class)).thenReturn(100L); Long feedbackId = feedbackMapper.addFeedback(feedback); assertEquals(100L, feedbackId); verify(jdbc) .update( - eq( - "INSERT INTO feedback (" - + "reviewer_id, reviewee_id, mentorship_cycle_id, feedback_type_id, " - + "rating, feedback_text, feedback_year, is_anonymous, is_approved) " - + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"), - eq(1L), - eq(2L), - eq(5L), - eq(1), - eq(5), - eq("Excellent mentor!"), - eq(2026), - eq(true), - eq(false)); - verify(jdbc).queryForObject(eq("SELECT LASTVAL()"), eq(Long.class)); + "INSERT INTO feedback (" + + "reviewer_id, reviewee_id, mentorship_cycle_id, feedback_type_id, " + + "rating, feedback_text, feedback_year, is_anonymous, is_approved) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + 1L, + 2L, + 5L, + 1, + 5, + "Excellent mentor!", + 2026, + true, + false); + verify(jdbc).queryForObject("SELECT LASTVAL()", Long.class); } @Test @@ -193,27 +191,26 @@ void testAddFeedbackWithNullableFields() { .isApproved(true) .build(); - when(jdbc.queryForObject(eq("SELECT LASTVAL()"), eq(Long.class))).thenReturn(200L); + when(jdbc.queryForObject("SELECT LASTVAL()", Long.class)).thenReturn(200L); Long feedbackId = feedbackMapper.addFeedback(feedback); assertEquals(200L, feedbackId); verify(jdbc) .update( - eq( - "INSERT INTO feedback (" - + "reviewer_id, reviewee_id, mentorship_cycle_id, feedback_type_id, " - + "rating, feedback_text, feedback_year, is_anonymous, is_approved) " - + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"), - eq(1L), - eq(null), - eq(null), - eq(2), - eq(null), - eq("Great community!"), - eq(null), - eq(false), - eq(true)); + "INSERT INTO feedback (" + + "reviewer_id, reviewee_id, mentorship_cycle_id, feedback_type_id, " + + "rating, feedback_text, feedback_year, is_anonymous, is_approved) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + 1L, + null, + null, + 2, + null, + "Great community!", + null, + false, + true); } @Test @@ -236,23 +233,22 @@ void testUpdateFeedback() { verify(jdbc) .update( - eq( - "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 = ?"), - eq(1L), - eq(2L), - eq(5L), - eq(1), - eq(4), - eq("Updated feedback text"), - eq(2026), - eq(false), - eq(true), - eq(feedbackId)); + "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 = ?", + 1L, + 2L, + 5L, + 1, + 4, + "Updated feedback text", + 2026, + false, + true, + feedbackId); } @Test @@ -271,22 +267,21 @@ void testUpdateFeedbackWithNullableFields() { verify(jdbc) .update( - eq( - "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 = ?"), - eq(10L), - eq(null), - eq(null), - eq(3), - eq(null), - eq("Updated program feedback"), - eq(null), - eq(true), - eq(false), - eq(feedbackId)); + "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 = ?", + 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 index bd090aba4..7c892b71c 100644 --- a/src/test/java/com/wcc/platform/service/FeedbackServiceTest.java +++ b/src/test/java/com/wcc/platform/service/FeedbackServiceTest.java @@ -245,8 +245,7 @@ void testGetAllFeedbackNullCriteria() { List result = service.getAllFeedback(null); - assertThat(result).isNotNull(); - assertThat(result).hasSize(2); + assertThat(result).isNotNull().hasSize(2); assertEquals(expectedList, result); verify(feedbackRepository).getAll(null); } @@ -263,8 +262,7 @@ void testGetAllFeedbackEmptyCriteria() { List result = service.getAllFeedback(criteria); - assertThat(result).isNotNull(); - assertThat(result).hasSize(2); + assertThat(result).isNotNull().hasSize(2); assertEquals(expectedList, result); verify(feedbackRepository).getAll(criteria); } @@ -293,8 +291,7 @@ void testGetAllFeedbackMultipleCriteria() { List result = service.getAllFeedback(criteria); - assertThat(result).isNotNull(); - assertThat(result).hasSize(2); + assertThat(result).isNotNull().hasSize(2); assertEquals(expectedList, result); verify(memberRepository).findById(reviewerId); verify(memberRepository).findById(revieweeId); From 07d36145f2f417b4aac289b85853bc972299140b Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Wed, 8 Apr 2026 21:51:38 +0200 Subject: [PATCH 08/17] Feedback API Implement PR suggestions - add @Valid to FeedbackDTO create and update - add @DsiaplyName to tests - update FeedbackMapper addFeedback() to query database instead of using SELECT LASTVAL() when returning values. - update tests - Update FeedbackType to throw exception if nonexisting typeId passed to fromId() function - Fix PostgresFeedbackRepository update() to return feedback object from database and not just the same entity that is passed to update() as arg. --- feedback-post-request.json | 8 -- .../controller/FeedbackController.java | 6 +- .../domain/platform/type/FeedbackType.java | 2 +- .../postgres/PostgresFeedbackRepository.java | 2 +- .../postgres/component/FeedbackMapper.java | 14 ++-- .../controller/FeedbackControllerTest.java | 33 ++++++++ .../validation/FeedbackDtoValidationTest.java | 17 ++++ .../PostgresFeedbackRepositoryTest.java | 39 +++++++++ .../component/FeedbackMapperTest.java | 79 ++++++------------- 9 files changed, 128 insertions(+), 72 deletions(-) delete mode 100644 feedback-post-request.json diff --git a/feedback-post-request.json b/feedback-post-request.json deleted file mode 100644 index d1c94b6ba..000000000 --- a/feedback-post-request.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "reviewerId": 1, - "feedbackType": "COMMUNITY_GENERAL", - "rating": 4, - "feedbackText": "Great community with excellent mentors and structured programs. The mentorship cycle is well organized and everyone is supportive.", - "year": 2026 -} - diff --git a/src/main/java/com/wcc/platform/controller/FeedbackController.java b/src/main/java/com/wcc/platform/controller/FeedbackController.java index 1a7c522ad..79a6ed2d3 100644 --- a/src/main/java/com/wcc/platform/controller/FeedbackController.java +++ b/src/main/java/com/wcc/platform/controller/FeedbackController.java @@ -9,6 +9,7 @@ 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 java.util.List; import lombok.AllArgsConstructor; import org.springframework.http.HttpStatus; @@ -101,7 +102,8 @@ public ResponseEntity getFeedbackById( @PostMapping @Operation(summary = "Create feedback") @ResponseStatus(HttpStatus.CREATED) - public ResponseEntity createFeedback(@RequestBody final FeedbackDto feedbackDto) { + public ResponseEntity createFeedback( + @Valid @RequestBody final FeedbackDto feedbackDto) { final Feedback feedback = feedbackService.createFeedback(feedbackDto); return new ResponseEntity<>(feedback, HttpStatus.CREATED); } @@ -118,7 +120,7 @@ public ResponseEntity createFeedback(@RequestBody final FeedbackDto fe @ResponseStatus(HttpStatus.OK) public ResponseEntity updateFeedback( @Parameter(description = "ID of the feedback") @PathVariable final Long feedbackId, - @RequestBody final FeedbackDto feedbackDto) { + @Valid @RequestBody final FeedbackDto feedbackDto) { final Feedback feedback = feedbackService.updateFeedback(feedbackId, feedbackDto); return ResponseEntity.ok(feedback); } 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 index 3563ff555..323dab3bc 100644 --- a/src/main/java/com/wcc/platform/domain/platform/type/FeedbackType.java +++ b/src/main/java/com/wcc/platform/domain/platform/type/FeedbackType.java @@ -28,6 +28,6 @@ public static FeedbackType fromId(final int typeId) { return type; } } - return COMMUNITY_GENERAL; + throw new IllegalArgumentException("Unknown FeedbackType id: " + typeId); } } diff --git a/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java b/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java index c0b3310c0..acc305b8b 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java +++ b/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java @@ -41,7 +41,7 @@ public Feedback create(final Feedback entity) { @Override public Feedback update(final Long id, final Feedback entity) { feedbackMapper.updateFeedback(entity, id); - return entity; + return findById(id).orElseThrow(); } @Override 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 index 72e813824..8400a23be 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/component/FeedbackMapper.java +++ b/src/main/java/com/wcc/platform/repository/postgres/component/FeedbackMapper.java @@ -27,13 +27,15 @@ @RequiredArgsConstructor @SuppressWarnings("PMD.TooManyStaticImports") public class FeedbackMapper { - private static final String INSERT_SQL = + /* 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 (?, ?, ?, ?, ?, ?, ?, ?, ?)"; + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) RETURNING id"; - private static final String UPDATE_SQL = + /* default */ + static final String UPDATE_SQL = "UPDATE feedback SET " + "reviewer_id = ?, reviewee_id = ?, mentorship_cycle_id = ?, " + "feedback_type_id = ?, rating = ?, feedback_text = ?, " @@ -75,8 +77,9 @@ public Feedback mapRowToFeedback(final ResultSet rs) throws SQLException { /** Adds a new feedback to the database and returns the feedback ID. */ public Long addFeedback(final Feedback feedback) { - jdbc.update( + return jdbc.queryForObject( INSERT_SQL, + Long.class, feedback.getReviewerId(), feedback.getRevieweeId(), feedback.getMentorshipCycleId(), @@ -86,9 +89,6 @@ public Long addFeedback(final Feedback feedback) { feedback.getYear(), feedback.getIsAnonymous(), feedback.getIsApproved()); - - // Return the last inserted ID - return jdbc.queryForObject("SELECT LASTVAL()", Long.class); } /** diff --git a/src/test/java/com/wcc/platform/controller/FeedbackControllerTest.java b/src/test/java/com/wcc/platform/controller/FeedbackControllerTest.java index d68262024..d479eb4b9 100644 --- a/src/test/java/com/wcc/platform/controller/FeedbackControllerTest.java +++ b/src/test/java/com/wcc/platform/controller/FeedbackControllerTest.java @@ -31,6 +31,7 @@ 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; @@ -55,6 +56,8 @@ class FeedbackControllerTest { @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(); @@ -71,6 +74,8 @@ void testGetFeedbackByIdReturnsOk() throws Exception { } @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)) @@ -82,6 +87,8 @@ void testGetFeedbackByIdNotFound() throws Exception { } @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(); @@ -99,6 +106,8 @@ void testCreateFeedbackReturnsCreated() throws Exception { } @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(); @@ -114,6 +123,8 @@ void testCreateCommunityFeedbackReturnsCreated() throws Exception { } @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(); @@ -139,6 +150,7 @@ void testUpdateFeedbackReturnsOk() throws Exception { } @Test + @DisplayName("Given non-existent feedback ID, when updating feedback, then returns not found") void testUpdateNonExistentFeedbackThrowsException() throws Exception { Long nonExistentFeedbackId = 999L; FeedbackDto feedbackDto = createMentorReviewFeedbackDtoTest(); @@ -156,6 +168,7 @@ void testUpdateNonExistentFeedbackThrowsException() throws Exception { } @Test + @DisplayName("Given valid feedback ID, when approving feedback, then returns OK") void testApproveFeedbackReturnsOk() throws Exception { Long feedbackId = 1L; doNothing().when(feedbackService).approveFeedback(feedbackId); @@ -170,6 +183,7 @@ void testApproveFeedbackReturnsOk() throws Exception { } @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)) @@ -184,6 +198,8 @@ void testApproveNonExistentFeedbackThrowsException() throws Exception { } @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; @@ -200,6 +216,8 @@ void testUpdateFeedbackAnonymousStatusReturnsOk() throws Exception { } @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; @@ -216,6 +234,7 @@ void testUpdateFeedbackAnonymousStatusToFalseReturnsOk() throws Exception { } @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); @@ -230,6 +249,7 @@ void testDeleteFeedbackReturnsNoContent() throws Exception { } @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)) @@ -244,6 +264,7 @@ void testDeleteNonExistentFeedbackThrowsException() throws Exception { } @Test + @DisplayName("Given no query filters, when getting all feedback, then returns all feedback") void testGetAllFeedbackNoFilters() throws Exception { Feedback feedback1 = createMentorReviewFeedbackTest(); Feedback feedback2 = createCommunityGeneralFeedbackTest(); @@ -275,6 +296,8 @@ void testGetAllFeedbackNoFilters() throws Exception { } @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(); @@ -305,6 +328,8 @@ void testGetAllFeedbackWithReviewerId() throws Exception { } @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(); @@ -332,6 +357,8 @@ void testGetAllFeedbackWithRevieweeId() throws Exception { } @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(); @@ -361,6 +388,8 @@ void testGetAllFeedbackWithType() throws Exception { } @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(); @@ -389,6 +418,8 @@ void testGetAllFeedbackWithYear() throws Exception { } @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; @@ -421,6 +452,8 @@ void testGetAllFeedbackMultipleFilters() throws Exception { } @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())); 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 index 8d6159973..f934e8376 100644 --- 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 @@ -12,6 +12,7 @@ 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. */ @@ -26,6 +27,7 @@ void setUp() { } @Test + @DisplayName("Given valid mentor review feedback DTO, when validating, then validation passes") void testValidMentorReviewFeedback() { FeedbackDto dto = FeedbackDto.builder() @@ -43,6 +45,7 @@ void testValidMentorReviewFeedback() { } @Test + @DisplayName("Given mentor review without reviewee ID, when validating, then validation fails") void testMentorReviewWithoutRevieweeIdFails() { FeedbackDto dto = FeedbackDto.builder() @@ -63,6 +66,8 @@ void testMentorReviewWithoutRevieweeIdFails() { } @Test + @DisplayName( + "Given mentor review without mentorship cycle ID, when validating, then validation fails") void testMentorReviewWithoutMentorshipCycleIdFails() { FeedbackDto dto = FeedbackDto.builder() @@ -83,6 +88,8 @@ void testMentorReviewWithoutMentorshipCycleIdFails() { } @Test + @DisplayName( + "Given valid mentorship program feedback DTO, when validating, then validation passes") void testValidMentorshipProgramFeedback() { FeedbackDto dto = FeedbackDto.builder() @@ -99,6 +106,7 @@ void testValidMentorshipProgramFeedback() { } @Test + @DisplayName("Given mentorship program without cycle ID, when validating, then validation fails") void testMentorshipProgramWithoutCycleIdFails() { FeedbackDto dto = FeedbackDto.builder() @@ -121,6 +129,7 @@ void testMentorshipProgramWithoutCycleIdFails() { } @Test + @DisplayName("Given mentor review without rating, when validating, then validation fails") void testMentorReviewWithoutRatingFails() { FeedbackDto dto = FeedbackDto.builder() @@ -141,6 +150,7 @@ void testMentorReviewWithoutRatingFails() { } @Test + @DisplayName("Given mentorship program without rating, when validating, then validation fails") void testMentorshipProgramWithoutRatingFails() { FeedbackDto dto = FeedbackDto.builder() @@ -160,6 +170,8 @@ void testMentorshipProgramWithoutRatingFails() { } @Test + @DisplayName( + "Given valid community general feedback DTO, when validating, then validation passes") void testValidCommunityGeneralFeedback() { FeedbackDto dto = FeedbackDto.builder() @@ -175,6 +187,8 @@ void testValidCommunityGeneralFeedback() { } @Test + @DisplayName( + "Given community general feedback without rating, when validating, then validation passes") void testValidCommunityGeneralFeedbackWithoutRating() { FeedbackDto dto = FeedbackDto.builder() @@ -191,6 +205,7 @@ void testValidCommunityGeneralFeedbackWithoutRating() { } @Test + @DisplayName("Given DTO with missing required fields, when validating, then validation fails") void testMissingRequiredFieldsFails() { FeedbackDto dto = FeedbackDto.builder().build(); @@ -201,6 +216,7 @@ void testMissingRequiredFieldsFails() { } @Test + @DisplayName("Given DTO with invalid rating value, when validating, then validation fails") void testInvalidRatingFails() { FeedbackDto dto = FeedbackDto.builder() @@ -217,6 +233,7 @@ void testInvalidRatingFails() { } @Test + @DisplayName("Given DTO with blank feedback text, when validating, then validation fails") void testBlankFeedbackTextFails() { FeedbackDto dto = FeedbackDto.builder() diff --git a/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java b/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java index 0d878bc73..96675bac8 100644 --- a/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java +++ b/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java @@ -22,6 +22,7 @@ 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; @@ -49,6 +50,7 @@ void setUp() { } @Test + @DisplayName("Given valid feedback, when creating, then returns created feedback with ID") void testCreate() { Feedback feedback = createMentorReviewFeedbackTest(); when(feedbackMapper.addFeedback(any())).thenReturn(1L); @@ -63,6 +65,8 @@ void testCreate() { } @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); @@ -72,10 +76,12 @@ void testCreateThrowsWhenFindByIdEmpty() { } @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); @@ -85,6 +91,7 @@ void testUpdate() { } @Test + @DisplayName("Given valid feedback ID, when finding by ID, then returns feedback") void testFindById() { Long feedbackId = 1L; Feedback feedback = createMentorReviewFeedbackTest(); @@ -100,6 +107,7 @@ void testFindById() { } @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))) @@ -112,6 +120,7 @@ void testFindByIdNotFound() { } @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); @@ -122,6 +131,7 @@ void testDeleteById() { } @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); @@ -132,6 +142,8 @@ void testApproveFeedback() { } @Test + @DisplayName( + "Given feedback ID and anonymous true, when updating anonymous status, then executes update") void testUpdateAnonymousStatus() { Long feedbackId = 1L; Boolean isAnonymous = true; @@ -143,6 +155,8 @@ void testUpdateAnonymousStatus() { } @Test + @DisplayName( + "Given feedback ID and anonymous false, when updating anonymous status, then executes update") void testUpdateAnonymousStatusToFalse() { Long feedbackId = 1L; Boolean isAnonymous = false; @@ -154,6 +168,7 @@ void testUpdateAnonymousStatusToFalse() { } @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))) @@ -163,6 +178,8 @@ void testFindByIdJdbcThrows() { } @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(Object[].class))) @@ -176,6 +193,7 @@ void testGetAllThrowsFeedbackNotFoundException() { } @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); @@ -186,6 +204,7 @@ void testDeleteByIdNonExistent() { } @Test + @DisplayName("Given null criteria, when getting all, then returns all feedback") void testGetAllNullCriteria() { when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) .thenReturn(java.util.Collections.emptyList()); @@ -198,6 +217,7 @@ void testGetAllNullCriteria() { } @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(Object[].class))) @@ -212,6 +232,7 @@ void testGetAllEmpty() { } @Test + @DisplayName("Given multiple search criteria, when getting all, then returns matching feedback") void testGetAllWithMultipleCriteria() { Long reviewerId = 1L; Long revieweeId = 2L; @@ -243,6 +264,7 @@ void testGetAllWithMultipleCriteria() { } @Test + @DisplayName("Given reviewer ID filter, when getting all, then returns feedback by reviewer") void testGetAllWithReviewerIdOnly() { Long reviewerId = 1L; FeedbackSearchCriteria criteria = @@ -264,6 +286,7 @@ void testGetAllWithReviewerIdOnly() { } @Test + @DisplayName("Given reviewee ID filter, when getting all, then returns feedback by reviewee") void testGetAllWithRevieweeIdOnly() { Long revieweeId = 2L; FeedbackSearchCriteria criteria = @@ -285,6 +308,8 @@ void testGetAllWithRevieweeIdOnly() { } @Test + @DisplayName( + "Given feedback type filter, when getting all, then returns feedback of specified type") void testGetAllWithFeedbackTypeOnly() { FeedbackSearchCriteria criteria = FeedbackSearchCriteria.builder() @@ -307,6 +332,7 @@ void testGetAllWithFeedbackTypeOnly() { } @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(); @@ -327,6 +353,8 @@ void testGetAllWithYearOnly() { } @Test + @DisplayName( + "Given mentorship cycle ID filter, when getting all, then returns feedback for cycle") void testGetAllWithMentorshipCycleIdOnly() { Long mentorshipCycleId = 5L; FeedbackSearchCriteria criteria = @@ -348,6 +376,7 @@ void testGetAllWithMentorshipCycleIdOnly() { } @Test + @DisplayName("Given isApproved true filter, when getting all, then returns approved feedback") void testGetAllWithIsApprovedTrue() { Boolean isApproved = true; FeedbackSearchCriteria criteria = @@ -369,6 +398,7 @@ void testGetAllWithIsApprovedTrue() { } @Test + @DisplayName("Given isApproved false filter, when getting all, then returns unapproved feedback") void testGetAllWithIsApprovedFalse() { Boolean isApproved = false; FeedbackSearchCriteria criteria = @@ -390,6 +420,7 @@ void testGetAllWithIsApprovedFalse() { } @Test + @DisplayName("Given isAnonymous true filter, when getting all, then returns anonymous feedback") void testGetAllWithIsAnonymousTrue() { Boolean isAnonymous = true; FeedbackSearchCriteria criteria = @@ -411,6 +442,8 @@ void testGetAllWithIsAnonymousTrue() { } @Test + @DisplayName( + "Given isAnonymous false filter, when getting all, then returns non-anonymous feedback") void testGetAllWithIsAnonymousFalse() { Boolean isAnonymous = false; FeedbackSearchCriteria criteria = @@ -432,6 +465,8 @@ void testGetAllWithIsAnonymousFalse() { } @Test + @DisplayName( + "Given all search criteria, when getting all, then returns feedback matching all filters") void testGetAllWithAllCriteria() { Long reviewerId = 1L; Long revieweeId = 2L; @@ -472,6 +507,8 @@ void testGetAllWithAllCriteria() { } @Test + @DisplayName( + "Given feedback type and year filters, when getting all, then returns matching feedback") void testGetAllWithFeedbackTypeAndYear() { Integer year = 2026; FeedbackSearchCriteria criteria = @@ -495,6 +532,8 @@ void testGetAllWithFeedbackTypeAndYear() { } @Test + @DisplayName( + "Given mentorship cycle and approved filters, when getting all, then returns matching feedback") void testGetAllWithMentorshipCycleAndApproved() { Long mentorshipCycleId = 10L; Boolean isApproved = true; 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 index ab4d68cc8..efa22c53a 100644 --- a/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java +++ b/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java @@ -1,5 +1,7 @@ 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.COLUMN_CREATED_AT; import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_FEEDBACK_TEXT; import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_FEEDBACK_TYPE_ID; @@ -25,6 +27,7 @@ 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; @@ -45,6 +48,8 @@ void setUp() { } @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; @@ -98,6 +103,8 @@ void testMapRowToFeedback() throws SQLException { } @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; @@ -137,12 +144,14 @@ void testMapRowToFeedbackWithNullableFields() throws SQLException { } @Test + @DisplayName("Given ResultSet throws SQLException, when mapping, then propagates exception") void handlesSqlExceptionGracefully() throws Exception { when(resultSet.getLong(COLUMN_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() @@ -157,30 +166,21 @@ void testAddFeedback() { .isApproved(false) .build(); - when(jdbc.queryForObject("SELECT LASTVAL()", Long.class)).thenReturn(100L); + 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) - .update( - "INSERT INTO feedback (" - + "reviewer_id, reviewee_id, mentorship_cycle_id, feedback_type_id, " - + "rating, feedback_text, feedback_year, is_anonymous, is_approved) " - + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", - 1L, - 2L, - 5L, - 1, - 5, - "Excellent mentor!", - 2026, - true, - false); - verify(jdbc).queryForObject("SELECT LASTVAL()", Long.class); + .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() @@ -191,29 +191,20 @@ void testAddFeedbackWithNullableFields() { .isApproved(true) .build(); - when(jdbc.queryForObject("SELECT LASTVAL()", Long.class)).thenReturn(200L); + 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) - .update( - "INSERT INTO feedback (" - + "reviewer_id, reviewee_id, mentorship_cycle_id, feedback_type_id, " - + "rating, feedback_text, feedback_year, is_anonymous, is_approved) " - + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", - 1L, - null, - null, - 2, - null, - "Great community!", - null, - false, - true); + .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 = @@ -233,25 +224,12 @@ void testUpdateFeedback() { verify(jdbc) .update( - "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 = ?", - 1L, - 2L, - 5L, - 1, - 4, - "Updated feedback text", - 2026, - false, - true, - feedbackId); + 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 = @@ -267,12 +245,7 @@ void testUpdateFeedbackWithNullableFields() { verify(jdbc) .update( - "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 = ?", + UPDATE_SQL, 10L, null, null, From 267cd094ff7fe8a0150d7db841320b5243b70df0 Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Sat, 11 Apr 2026 11:54:44 +0200 Subject: [PATCH 09/17] Feedback API - Update of migration file. Change rating range and add constraint. --- .../db/migration/V36__20260406__create_feedback_system.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 index 8c9e6a0bb..7845e3522 100644 --- a/src/main/resources/db/migration/V36__20260406__create_feedback_system.sql +++ b/src/main/resources/db/migration/V36__20260406__create_feedback_system.sql @@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS feedback 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 >= 0 AND rating <= 5), + rating INTEGER CHECK (rating >= 1 AND rating <= 5), feedback_text TEXT NOT NULL, feedback_year INTEGER, is_anonymous BOOLEAN DEFAULT TRUE, @@ -32,6 +32,10 @@ CREATE TABLE IF NOT EXISTS feedback 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 ) ); From cc261de317f3505c52c2a20d3d2f531283befc98 Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Sat, 18 Apr 2026 18:26:19 +0200 Subject: [PATCH 10/17] Feedback API Update FeedbackMapper - set reviewer and reviewee names instead of nulls. Update FeedbackMapper tests. Update Feedback - Remove Setter for Reviewer Name and Reviewee Name. Update PostgresFeedbackRepository SELECT sql queries to join members and feedback to retrieve member's names. Update tests to reflect join between members and feedback tables. Update application.yml to contain also option to host admin app on 3001 port. --- .../domain/platform/feedback/Feedback.java | 4 +-- .../postgres/PostgresFeedbackRepository.java | 14 ++++++-- .../postgres/component/FeedbackMapper.java | 6 ++-- .../postgres/constants/FeedbackConstants.java | 2 ++ .../wcc/platform/service/FeedbackService.java | 26 ++++++-------- src/main/resources/application.yml | 2 +- .../platform/feedback/FeedbackTest.java | 4 --- .../PostgresFeedbackRepositoryTest.java | 36 +++++++++++-------- .../component/FeedbackMapperTest.java | 10 ++++-- 9 files changed, 60 insertions(+), 44 deletions(-) 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 index c3a25dfca..a7e9fc4b3 100644 --- a/src/main/java/com/wcc/platform/domain/platform/feedback/Feedback.java +++ b/src/main/java/com/wcc/platform/domain/platform/feedback/Feedback.java @@ -25,9 +25,9 @@ public class Feedback { @Setter private Long id; @NotNull private Long reviewerId; - @Setter private String reviewerName; + private String reviewerName; private Long revieweeId; // For MENTOR_REVIEW - @Setter private String revieweeName; + private String revieweeName; private Long mentorshipCycleId; // For MENTORSHIP_PROGRAM @NotNull private FeedbackType feedbackType; diff --git a/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java b/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java index acc305b8b..bde33421a 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java +++ b/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java @@ -23,7 +23,11 @@ public class PostgresFeedbackRepository implements FeedbackRepository { private static final String DELETE_SQL = "DELETE FROM feedback WHERE id = ?"; - private static final String SELECT_BY_ID = "SELECT * 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 = @@ -65,7 +69,13 @@ public void deleteById(final Long feedbackId) { @Override @SuppressWarnings({"PMD.InsufficientStringBufferDeclaration", "PMD.CognitiveComplexity"}) public List getAll(final FeedbackSearchCriteria criteria) { - final StringBuilder sql = new StringBuilder("SELECT * FROM feedback WHERE 1=1"); + 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) { 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 index 8400a23be..29f2e3d80 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/component/FeedbackMapper.java +++ b/src/main/java/com/wcc/platform/repository/postgres/component/FeedbackMapper.java @@ -9,7 +9,9 @@ import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_MENTORSHIP_CYCLE_ID; import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_RATING; import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWEE_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWEE_NAME; import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWER_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWER_NAME; import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_UPDATED_AT; import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_YEAR; @@ -50,10 +52,10 @@ public Feedback mapRowToFeedback(final ResultSet rs) throws SQLException { return Feedback.builder() .id(rs.getLong(COLUMN_ID)) .reviewerId(rs.getLong(COLUMN_REVIEWER_ID)) - .reviewerName(null) + .reviewerName(rs.getString(COLUMN_REVIEWER_NAME)) .revieweeId( rs.getObject(COLUMN_REVIEWEE_ID) != null ? rs.getLong(COLUMN_REVIEWEE_ID) : null) - .revieweeName(null) + .revieweeName(rs.getString(COLUMN_REVIEWEE_NAME)) .mentorshipCycleId( rs.getObject(COLUMN_MENTORSHIP_CYCLE_ID) != null ? rs.getLong(COLUMN_MENTORSHIP_CYCLE_ID) 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 index b67c28b5e..7005db465 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/constants/FeedbackConstants.java +++ b/src/main/java/com/wcc/platform/repository/postgres/constants/FeedbackConstants.java @@ -8,6 +8,8 @@ public final class FeedbackConstants { public static final String COLUMN_ID = "id"; public static final String COLUMN_REVIEWER_ID = "reviewer_id"; public static final String COLUMN_REVIEWEE_ID = "reviewee_id"; + public static final String COLUMN_REVIEWER_NAME = "reviewer_name"; + public static final String COLUMN_REVIEWEE_NAME = "reviewee_name"; public static final String COLUMN_MENTORSHIP_CYCLE_ID = "mentorship_cycle_id"; public static final String COLUMN_FEEDBACK_TYPE_ID = "feedback_type_id"; public static final String COLUMN_RATING = "rating"; diff --git a/src/main/java/com/wcc/platform/service/FeedbackService.java b/src/main/java/com/wcc/platform/service/FeedbackService.java index 8a9463db0..1c60d31ce 100644 --- a/src/main/java/com/wcc/platform/service/FeedbackService.java +++ b/src/main/java/com/wcc/platform/service/FeedbackService.java @@ -5,7 +5,6 @@ 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; @@ -32,14 +31,11 @@ public FeedbackService( * @return created Feedback */ public Feedback createFeedback(final FeedbackDto feedbackDto) { - final Member reviewer = validateReviewerExists(feedbackDto.getReviewerId()); + validateReviewerExists(feedbackDto.getReviewerId()); final Feedback feedback = feedbackDto.merge(); - feedback.setReviewerName(reviewer.getFullName()); - if (feedback.getRevieweeId() != null) { - final Member reviewee = validateRevieweeExists(feedback.getRevieweeId()); - feedback.setRevieweeName(reviewee.getFullName()); + validateRevieweeExists(feedback.getRevieweeId()); } log.info( @@ -62,18 +58,16 @@ public Feedback updateFeedback(final Long feedbackId, final FeedbackDto feedback .findById(feedbackId) .orElseThrow(() -> new FeedbackNotFoundException(feedbackId)); - final Member reviewer = validateReviewerExists(feedbackDto.getReviewerId()); + validateReviewerExists(feedbackDto.getReviewerId()); final Feedback updatedFeedback = feedbackDto.merge(); updatedFeedback.setId(feedbackId); updatedFeedback.setIsApproved(existing.getIsApproved()); updatedFeedback.setIsAnonymous(existing.getIsAnonymous()); - updatedFeedback.setReviewerName(reviewer.getFullName()); if (updatedFeedback.getRevieweeId() != null) { - final Member reviewee = validateRevieweeExists(updatedFeedback.getRevieweeId()); - updatedFeedback.setRevieweeName(reviewee.getFullName()); + validateRevieweeExists(updatedFeedback.getRevieweeId()); } return feedbackRepository.update(feedbackId, updatedFeedback); @@ -137,10 +131,10 @@ public List getAllFeedback(final FeedbackSearchCriteria criteria) { /** * Validate that reviewer with given ID exists. * - * @return Member + * @return void, throws exception if reviewer not found */ - private Member validateReviewerExists(final Long reviewerId) { - return memberRepository + private void validateReviewerExists(final Long reviewerId) { + memberRepository .findById(reviewerId) .orElseThrow( () -> { @@ -152,10 +146,10 @@ private Member validateReviewerExists(final Long reviewerId) { /** * Validate that reviewee with given ID exists. * - * @return Member + * @return void, throws exception if reviewee not found */ - private Member validateRevieweeExists(final Long revieweeId) { - return memberRepository + private void validateRevieweeExists(final Long revieweeId) { + memberRepository .findById(revieweeId) .orElseThrow( () -> { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 69ad343c3..85cf589dd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -64,7 +64,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/test/java/com/wcc/platform/domain/platform/feedback/FeedbackTest.java b/src/test/java/com/wcc/platform/domain/platform/feedback/FeedbackTest.java index 58cf10925..f776e0432 100644 --- a/src/test/java/com/wcc/platform/domain/platform/feedback/FeedbackTest.java +++ b/src/test/java/com/wcc/platform/domain/platform/feedback/FeedbackTest.java @@ -168,14 +168,10 @@ void testToBuilder() { void testSetters() { // Only test setters that are actually available in production code feedback.setId(99L); - feedback.setReviewerName("New Reviewer"); - feedback.setRevieweeName("New Reviewee"); feedback.setIsAnonymous(false); feedback.setIsApproved(true); assertEquals(99L, feedback.getId()); - assertEquals("New Reviewer", feedback.getReviewerName()); - assertEquals("New Reviewee", feedback.getRevieweeName()); assertFalse(feedback.getIsAnonymous()); assertTrue(feedback.getIsApproved()); } diff --git a/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java b/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java index 96675bac8..9c0f5ca54 100644 --- a/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java +++ b/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java @@ -37,6 +37,12 @@ class PostgresFeedbackRepositoryTest { "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; @@ -213,7 +219,7 @@ void testGetAllNullCriteria() { assertNotNull(result); verify(jdbc) - .query(eq("SELECT * FROM feedback WHERE 1=1"), any(RowMapper.class), eq(new Object[0])); + .query(eq(GET_ALL_BASE), any(RowMapper.class), eq(new Object[0])); } @Test @@ -228,7 +234,7 @@ void testGetAllEmpty() { assertNotNull(result); assertTrue(result.isEmpty()); verify(jdbc) - .query(eq("SELECT * FROM feedback WHERE 1=1"), any(RowMapper.class), eq(new Object[0])); + .query(eq(GET_ALL_BASE), any(RowMapper.class), eq(new Object[0])); } @Test @@ -258,7 +264,7 @@ void testGetAllWithMultipleCriteria() { verify(jdbc) .query( eq( - "SELECT * FROM feedback WHERE 1=1 AND reviewer_id = ? AND reviewee_id = ? AND feedback_year = ?"), + GET_ALL_BASE + " AND reviewer_id = ? AND reviewee_id = ? AND feedback_year = ?"), any(RowMapper.class), eq(new Object[] {reviewerId, revieweeId, year})); } @@ -280,7 +286,7 @@ void testGetAllWithReviewerIdOnly() { assertEquals(1, result.size()); verify(jdbc) .query( - eq("SELECT * FROM feedback WHERE 1=1 AND reviewer_id = ?"), + eq(GET_ALL_BASE + " AND reviewer_id = ?"), any(RowMapper.class), eq(new Object[] {reviewerId})); } @@ -302,7 +308,7 @@ void testGetAllWithRevieweeIdOnly() { assertEquals(1, result.size()); verify(jdbc) .query( - eq("SELECT * FROM feedback WHERE 1=1 AND reviewee_id = ?"), + eq(GET_ALL_BASE + " AND reviewee_id = ?"), any(RowMapper.class), eq(new Object[] {revieweeId})); } @@ -326,7 +332,7 @@ void testGetAllWithFeedbackTypeOnly() { assertEquals(1, result.size()); verify(jdbc) .query( - eq("SELECT * FROM feedback WHERE 1=1 AND feedback_type_id = ?"), + eq(GET_ALL_BASE + " AND feedback_type_id = ?"), any(RowMapper.class), eq(new Object[] {1})); } @@ -347,7 +353,7 @@ void testGetAllWithYearOnly() { assertEquals(1, result.size()); verify(jdbc) .query( - eq("SELECT * FROM feedback WHERE 1=1 AND feedback_year = ?"), + eq(GET_ALL_BASE + " AND feedback_year = ?"), any(RowMapper.class), eq(new Object[] {year})); } @@ -370,7 +376,7 @@ void testGetAllWithMentorshipCycleIdOnly() { assertEquals(1, result.size()); verify(jdbc) .query( - eq("SELECT * FROM feedback WHERE 1=1 AND mentorship_cycle_id = ?"), + eq(GET_ALL_BASE + " AND mentorship_cycle_id = ?"), any(RowMapper.class), eq(new Object[] {mentorshipCycleId})); } @@ -392,7 +398,7 @@ void testGetAllWithIsApprovedTrue() { assertEquals(1, result.size()); verify(jdbc) .query( - eq("SELECT * FROM feedback WHERE 1=1 AND is_approved = ?"), + eq(GET_ALL_BASE + " AND is_approved = ?"), any(RowMapper.class), eq(new Object[] {isApproved})); } @@ -414,7 +420,7 @@ void testGetAllWithIsApprovedFalse() { assertEquals(1, result.size()); verify(jdbc) .query( - eq("SELECT * FROM feedback WHERE 1=1 AND is_approved = ?"), + eq(GET_ALL_BASE + " AND is_approved = ?"), any(RowMapper.class), eq(new Object[] {isApproved})); } @@ -436,7 +442,7 @@ void testGetAllWithIsAnonymousTrue() { assertEquals(1, result.size()); verify(jdbc) .query( - eq("SELECT * FROM feedback WHERE 1=1 AND is_anonymous = ?"), + eq(GET_ALL_BASE + " AND is_anonymous = ?"), any(RowMapper.class), eq(new Object[] {isAnonymous})); } @@ -459,7 +465,7 @@ void testGetAllWithIsAnonymousFalse() { assertEquals(1, result.size()); verify(jdbc) .query( - eq("SELECT * FROM feedback WHERE 1=1 AND is_anonymous = ?"), + eq(GET_ALL_BASE + " AND is_anonymous = ?"), any(RowMapper.class), eq(new Object[] {isAnonymous})); } @@ -496,7 +502,7 @@ void testGetAllWithAllCriteria() { verify(jdbc) .query( eq( - "SELECT * FROM feedback WHERE 1=1 AND reviewer_id = ? AND reviewee_id = ?" + 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), @@ -526,7 +532,7 @@ void testGetAllWithFeedbackTypeAndYear() { assertNotNull(result); verify(jdbc) .query( - eq("SELECT * FROM feedback WHERE 1=1 AND feedback_type_id = ? AND feedback_year = ?"), + eq(GET_ALL_BASE + " AND feedback_type_id = ? AND feedback_year = ?"), any(RowMapper.class), eq(new Object[] {2, year})); } @@ -552,7 +558,7 @@ void testGetAllWithMentorshipCycleAndApproved() { assertNotNull(result); verify(jdbc) .query( - eq("SELECT * FROM feedback WHERE 1=1 AND mentorship_cycle_id = ? AND is_approved = ?"), + 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 index efa22c53a..0060facc5 100644 --- a/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java +++ b/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java @@ -11,7 +11,9 @@ import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_MENTORSHIP_CYCLE_ID; import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_RATING; import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWEE_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWEE_NAME; import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWER_ID; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWER_NAME; import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_UPDATED_AT; import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_YEAR; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -66,8 +68,10 @@ void testMapRowToFeedback() throws SQLException { when(resultSet.getLong(COLUMN_ID)).thenReturn(feedbackId); when(resultSet.getLong(COLUMN_REVIEWER_ID)).thenReturn(reviewerId); + when(resultSet.getString(COLUMN_REVIEWER_NAME)).thenReturn("Reviewer Name"); when(resultSet.getObject(COLUMN_REVIEWEE_ID)).thenReturn(revieweeId); when(resultSet.getLong(COLUMN_REVIEWEE_ID)).thenReturn(revieweeId); + when(resultSet.getString(COLUMN_REVIEWEE_NAME)).thenReturn("Reviewee Name"); when(resultSet.getObject(COLUMN_MENTORSHIP_CYCLE_ID)).thenReturn(mentorshipCycleId); when(resultSet.getLong(COLUMN_MENTORSHIP_CYCLE_ID)).thenReturn(mentorshipCycleId); when(resultSet.getInt(COLUMN_FEEDBACK_TYPE_ID)).thenReturn(feedbackTypeId); @@ -98,8 +102,8 @@ void testMapRowToFeedback() throws SQLException { assertEquals(isApproved, feedback.getIsApproved()); assertEquals(createdAt, feedback.getCreatedAt()); assertEquals(updatedAt, feedback.getUpdatedAt()); - assertNull(feedback.getReviewerName()); - assertNull(feedback.getRevieweeName()); + assertEquals("Reviewer Name", feedback.getReviewerName()); + assertEquals("Reviewee Name", feedback.getRevieweeName()); } @Test @@ -115,7 +119,9 @@ void testMapRowToFeedbackWithNullableFields() throws SQLException { when(resultSet.getLong(COLUMN_ID)).thenReturn(feedbackId); when(resultSet.getLong(COLUMN_REVIEWER_ID)).thenReturn(reviewerId); + when(resultSet.getString(COLUMN_REVIEWER_NAME)).thenReturn("Reviewer Name"); when(resultSet.getObject(COLUMN_REVIEWEE_ID)).thenReturn(null); + when(resultSet.getString(COLUMN_REVIEWEE_NAME)).thenReturn(null); when(resultSet.getObject(COLUMN_MENTORSHIP_CYCLE_ID)).thenReturn(null); when(resultSet.getInt(COLUMN_FEEDBACK_TYPE_ID)).thenReturn(feedbackTypeId); when(resultSet.getObject(COLUMN_RATING)).thenReturn(null); From 71343cc865e1961cfbbc75d47973c01d17065653 Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Sat, 18 Apr 2026 18:54:47 +0200 Subject: [PATCH 11/17] Feedback API Implement PMD suggestions. --- .../wcc/platform/service/FeedbackService.java | 12 +--- .../platform/feedback/FeedbackTest.java | 6 -- .../PostgresFeedbackRepositoryTest.java | 60 +++++++++++-------- .../component/FeedbackMapperTest.java | 6 +- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/main/java/com/wcc/platform/service/FeedbackService.java b/src/main/java/com/wcc/platform/service/FeedbackService.java index 1c60d31ce..176027f86 100644 --- a/src/main/java/com/wcc/platform/service/FeedbackService.java +++ b/src/main/java/com/wcc/platform/service/FeedbackService.java @@ -128,11 +128,7 @@ public List getAllFeedback(final FeedbackSearchCriteria criteria) { return feedbackRepository.getAll(criteria); } - /** - * Validate that reviewer with given ID exists. - * - * @return void, throws exception if reviewer not found - */ + /** Validate that reviewer with given ID exists. */ private void validateReviewerExists(final Long reviewerId) { memberRepository .findById(reviewerId) @@ -143,11 +139,7 @@ private void validateReviewerExists(final Long reviewerId) { }); } - /** - * Validate that reviewee with given ID exists. - * - * @return void, throws exception if reviewee not found - */ + /** Validate that reviewee with given ID exists. */ private void validateRevieweeExists(final Long revieweeId) { memberRepository .findById(revieweeId) 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 index f776e0432..9b35bed56 100644 --- a/src/test/java/com/wcc/platform/domain/platform/feedback/FeedbackTest.java +++ b/src/test/java/com/wcc/platform/domain/platform/feedback/FeedbackTest.java @@ -272,10 +272,4 @@ void testNotEqualsWithNull() { void testNotEqualsWithDifferentClass() { assertNotEquals("Not a Feedback object", feedback); } - - @Test - @DisplayName("Given same feedback instance, when equals called, then returns true") - void testEqualsSameInstance() { - assertEquals(feedback, 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 index 9c0f5ca54..0ac53c6ec 100644 --- a/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java +++ b/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java @@ -188,7 +188,7 @@ void testFindByIdJdbcThrows() { "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(Object[].class))) + when(jdbc.query(anyString(), any(RowMapper.class), any())) .thenThrow( new com.wcc.platform.domain.exceptions.FeedbackNotFoundException( "Invalid search criteria")); @@ -212,29 +212,27 @@ void testDeleteByIdNonExistent() { @Test @DisplayName("Given null criteria, when getting all, then returns all feedback") void testGetAllNullCriteria() { - when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + 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])); + 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(Object[].class))) + 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])); + verify(jdbc).query(eq(GET_ALL_BASE), any(RowMapper.class), eq(new Object[0])); } @Test @@ -254,7 +252,10 @@ void testGetAllWithMultipleCriteria() { Feedback feedback2 = createMentorReviewFeedbackTest(); feedback2.setId(2L); - when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + 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); @@ -263,8 +264,7 @@ void testGetAllWithMultipleCriteria() { assertEquals(2, result.size()); verify(jdbc) .query( - eq( - GET_ALL_BASE + " AND reviewer_id = ? AND reviewee_id = ? AND feedback_year = ?"), + eq(GET_ALL_BASE + " AND reviewer_id = ? AND reviewee_id = ? AND feedback_year = ?"), any(RowMapper.class), eq(new Object[] {reviewerId, revieweeId, year})); } @@ -277,7 +277,7 @@ void testGetAllWithReviewerIdOnly() { FeedbackSearchCriteria.builder().reviewerId(reviewerId).build(); Feedback feedback = createMentorReviewFeedbackTest(); - when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + when(jdbc.query(anyString(), any(RowMapper.class), any())) .thenReturn(java.util.List.of(feedback)); var result = repository.getAll(criteria); @@ -299,7 +299,7 @@ void testGetAllWithRevieweeIdOnly() { FeedbackSearchCriteria.builder().revieweeId(revieweeId).build(); Feedback feedback = createMentorReviewFeedbackTest(); - when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + when(jdbc.query(anyString(), any(RowMapper.class), any())) .thenReturn(java.util.List.of(feedback)); var result = repository.getAll(criteria); @@ -323,7 +323,7 @@ void testGetAllWithFeedbackTypeOnly() { .build(); Feedback feedback = createMentorReviewFeedbackTest(); - when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + when(jdbc.query(anyString(), any(RowMapper.class), any())) .thenReturn(java.util.List.of(feedback)); var result = repository.getAll(criteria); @@ -344,7 +344,7 @@ void testGetAllWithYearOnly() { FeedbackSearchCriteria criteria = FeedbackSearchCriteria.builder().year(year).build(); Feedback feedback = createMentorReviewFeedbackTest(); - when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + when(jdbc.query(anyString(), any(RowMapper.class), any())) .thenReturn(java.util.List.of(feedback)); var result = repository.getAll(criteria); @@ -367,7 +367,7 @@ void testGetAllWithMentorshipCycleIdOnly() { FeedbackSearchCriteria.builder().mentorshipCycleId(mentorshipCycleId).build(); Feedback feedback = createMentorReviewFeedbackTest(); - when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + when(jdbc.query(anyString(), any(RowMapper.class), any())) .thenReturn(java.util.List.of(feedback)); var result = repository.getAll(criteria); @@ -389,7 +389,7 @@ void testGetAllWithIsApprovedTrue() { FeedbackSearchCriteria.builder().isApproved(isApproved).build(); Feedback feedback = createMentorReviewFeedbackTest(); - when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + when(jdbc.query(anyString(), any(RowMapper.class), any())) .thenReturn(java.util.List.of(feedback)); var result = repository.getAll(criteria); @@ -411,7 +411,7 @@ void testGetAllWithIsApprovedFalse() { FeedbackSearchCriteria.builder().isApproved(isApproved).build(); Feedback feedback = createMentorReviewFeedbackTest(); - when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + when(jdbc.query(anyString(), any(RowMapper.class), any())) .thenReturn(java.util.List.of(feedback)); var result = repository.getAll(criteria); @@ -433,7 +433,7 @@ void testGetAllWithIsAnonymousTrue() { FeedbackSearchCriteria.builder().isAnonymous(isAnonymous).build(); Feedback feedback = createMentorReviewFeedbackTest(); - when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + when(jdbc.query(anyString(), any(RowMapper.class), any())) .thenReturn(java.util.List.of(feedback)); var result = repository.getAll(criteria); @@ -456,7 +456,7 @@ void testGetAllWithIsAnonymousFalse() { FeedbackSearchCriteria.builder().isAnonymous(isAnonymous).build(); Feedback feedback = createMentorReviewFeedbackTest(); - when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + when(jdbc.query(anyString(), any(RowMapper.class), any())) .thenReturn(java.util.List.of(feedback)); var result = repository.getAll(criteria); @@ -492,7 +492,17 @@ void testGetAllWithAllCriteria() { .build(); Feedback feedback = createMentorReviewFeedbackTest(); - when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + 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); @@ -502,7 +512,8 @@ void testGetAllWithAllCriteria() { verify(jdbc) .query( eq( - GET_ALL_BASE + " AND reviewer_id = ? AND reviewee_id = ?" + 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), @@ -524,7 +535,7 @@ void testGetAllWithFeedbackTypeAndYear() { .build(); Feedback feedback = createMentorReviewFeedbackTest(); - when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + when(jdbc.query(anyString(), any(RowMapper.class), any())) .thenReturn(java.util.List.of(feedback)); var result = repository.getAll(criteria); @@ -539,7 +550,8 @@ void testGetAllWithFeedbackTypeAndYear() { @Test @DisplayName( - "Given mentorship cycle and approved filters, when getting all, then returns matching feedback") + "Given mentorship cycle and approved filters, when getting all," + + "then returns matching feedback") void testGetAllWithMentorshipCycleAndApproved() { Long mentorshipCycleId = 10L; Boolean isApproved = true; @@ -550,7 +562,7 @@ void testGetAllWithMentorshipCycleAndApproved() { .build(); Feedback feedback = createMentorReviewFeedbackTest(); - when(jdbc.query(anyString(), any(RowMapper.class), any(Object[].class))) + when(jdbc.query(anyString(), any(RowMapper.class), any())) .thenReturn(java.util.List.of(feedback)); var result = repository.getAll(criteria); 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 index 0060facc5..fef3f14b2 100644 --- a/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java +++ b/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java @@ -51,7 +51,8 @@ void setUp() { @Test @DisplayName( - "Given ResultSet with all fields, when mapping row to feedback, then returns complete feedback") + "Given ResultSet with all fields, when mapping row to feedback, " + + "then returns complete feedback") void testMapRowToFeedback() throws SQLException { Long feedbackId = 1L; Long reviewerId = 10L; @@ -108,7 +109,8 @@ void testMapRowToFeedback() throws SQLException { @Test @DisplayName( - "Given ResultSet with nullable fields, when mapping row to feedback, then returns feedback with nulls") + "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; From dd082943fd499910655ebd2deaeb85eee2062ea6 Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Sat, 18 Apr 2026 18:58:24 +0200 Subject: [PATCH 12/17] Feedback API Implement PMD suggestions. --- .../repository/postgres/PostgresFeedbackRepositoryTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java b/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java index 0ac53c6ec..714ad9c67 100644 --- a/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java +++ b/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java @@ -552,6 +552,7 @@ void testGetAllWithFeedbackTypeAndYear() { @DisplayName( "Given mentorship cycle and approved filters, when getting all," + "then returns matching feedback") + @SuppressWarnings("unchecked") void testGetAllWithMentorshipCycleAndApproved() { Long mentorshipCycleId = 10L; Boolean isApproved = true; From cb42ef7ee04d54a1b88d94c87e4f81f79d9c28ca Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Sat, 18 Apr 2026 19:13:56 +0200 Subject: [PATCH 13/17] Feedback API Implement PMD suggestions. --- .../repository/postgres/PostgresFeedbackRepositoryTest.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java b/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java index 714ad9c67..05449a12c 100644 --- a/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java +++ b/src/test/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepositoryTest.java @@ -29,7 +29,7 @@ import org.springframework.jdbc.core.RowMapper; /** PostgresFeedbackRepositoryTest class for testing the PostgresFeedbackRepository. */ -@SuppressWarnings("PMD.TooManyMethods") +@SuppressWarnings({"PMD.TooManyMethods", "unchecked"}) class PostgresFeedbackRepositoryTest { private static final String DELETE_SQL = "DELETE FROM feedback WHERE id = ?"; @@ -552,7 +552,6 @@ void testGetAllWithFeedbackTypeAndYear() { @DisplayName( "Given mentorship cycle and approved filters, when getting all," + "then returns matching feedback") - @SuppressWarnings("unchecked") void testGetAllWithMentorshipCycleAndApproved() { Long mentorshipCycleId = 10L; Boolean isApproved = true; From 725032cf6e16d1ff7c37026b5baf45480613cdf3 Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Sat, 18 Apr 2026 22:47:40 +0200 Subject: [PATCH 14/17] fix: Change package.json to Use Fixed Port and works on all environments - change dev amd start scripts to "dev": "next dev -p 3001" and start": "next start -p 3001", --- admin-wcc-app/package-lock.json | 30 +++++++++--------------------- admin-wcc-app/package.json | 4 ++-- 2 files changed, 11 insertions(+), 23 deletions(-) diff --git a/admin-wcc-app/package-lock.json b/admin-wcc-app/package-lock.json index be2b3dea7..8dc15c2ab 100644 --- a/admin-wcc-app/package-lock.json +++ b/admin-wcc-app/package-lock.json @@ -82,7 +82,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -646,7 +645,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -690,7 +688,6 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1491,7 +1488,6 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.5.0.tgz", "integrity": "sha512-yjvtXoFcrPLGtgKRxFaH6OQPtcLPhkloC0BML6rBG5UeldR0nPULR/2E2BfXdo5JNV7j7lOzrrLX2Qf/iSidow==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.26.0", "@mui/core-downloads-tracker": "^6.5.0", @@ -2102,7 +2098,8 @@ "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -2285,7 +2282,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2297,7 +2293,6 @@ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^18.0.0" } @@ -2377,7 +2372,6 @@ "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.55.0", "@typescript-eslint/types": "8.55.0", @@ -2879,7 +2873,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3434,7 +3427,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -4197,7 +4189,8 @@ "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -4546,7 +4539,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4716,7 +4708,6 @@ "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -6633,7 +6624,6 @@ "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "^29.7.0", "@jest/types": "^29.6.3", @@ -8394,6 +8384,7 @@ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -8627,7 +8618,6 @@ "integrity": "sha512-cDOtUSIeoOvt1skKNihdExWMTybx3exnvbFbb9ecZDIxlvIbREQzt9A5Km3Zn3PfU+IFjyYGsHS+lN9VInAGKA==", "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", "license": "MIT", - "peer": true, "dependencies": { "@next/env": "14.2.12", "@swc/helpers": "0.5.5", @@ -9204,6 +9194,7 @@ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -9219,6 +9210,7 @@ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=10" }, @@ -9231,7 +9223,8 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/prompts": { "version": "2.4.2", @@ -9337,7 +9330,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -9350,7 +9342,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -9364,7 +9355,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -10534,7 +10524,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -10845,7 +10834,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/admin-wcc-app/package.json b/admin-wcc-app/package.json index a00955253..3e08a9843 100644 --- a/admin-wcc-app/package.json +++ b/admin-wcc-app/package.json @@ -3,9 +3,9 @@ "private": true, "version": "0.1.0", "scripts": { - "dev": "next dev -p ${PORT:-3000}", + "dev": "next dev -p 3001", "build": "next build", - "start": "next start -p ${PORT:-3000}", + "start": "next start -p 3001", "lint": "next lint", "test": "jest", "format": "prettier --write .", From d27fdbd09091d7ffc19fab44d7d1085c85567747 Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Tue, 5 May 2026 22:11:45 +0200 Subject: [PATCH 15/17] feat: Feedback - small updates - Restrict Year parameter to min and max range (2000 - 2100). Validate Year already in controller. Update tests. - Shorter names for feedback column names constants - Remove Primary from PostgresFeedbackRepository class --- .../controller/FeedbackController.java | 7 +- .../domain/platform/feedback/Feedback.java | 4 + .../domain/platform/feedback/FeedbackDto.java | 4 + .../postgres/PostgresFeedbackRepository.java | 2 - .../postgres/component/FeedbackMapper.java | 63 ++++++------ .../postgres/constants/FeedbackConstants.java | 30 +++--- .../validation/FeedbackDtoValidationTest.java | 59 +++++++++++ .../component/FeedbackMapperTest.java | 98 +++++++++---------- 8 files changed, 168 insertions(+), 99 deletions(-) diff --git a/src/main/java/com/wcc/platform/controller/FeedbackController.java b/src/main/java/com/wcc/platform/controller/FeedbackController.java index 79a6ed2d3..35ad7db02 100644 --- a/src/main/java/com/wcc/platform/controller/FeedbackController.java +++ b/src/main/java/com/wcc/platform/controller/FeedbackController.java @@ -10,10 +10,13 @@ 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; @@ -32,6 +35,7 @@ @SecurityRequirement(name = "apiKey") @Tag(name = "Feedback", description = "Feedback management APIs") @AllArgsConstructor +@Validated public class FeedbackController { private final FeedbackService feedbackService; @@ -58,7 +62,8 @@ public ResponseEntity> getAllFeedback( final Long mentorshipCycleId, @Parameter(description = "Feedback Type") @RequestParam(required = false) final FeedbackType feedbackType, - @Parameter(description = "Year") @RequestParam(required = false) final Integer year, + @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) 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 index a7e9fc4b3..b5d88373d 100644 --- a/src/main/java/com/wcc/platform/domain/platform/feedback/Feedback.java +++ b/src/main/java/com/wcc/platform/domain/platform/feedback/Feedback.java @@ -36,7 +36,11 @@ public class Feedback { 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; 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 index b8e5eaa75..04c9f9c06 100644 --- a/src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackDto.java +++ b/src/main/java/com/wcc/platform/domain/platform/feedback/FeedbackDto.java @@ -29,7 +29,11 @@ public class FeedbackDto { private Integer rating; @NotBlank private String feedbackText; + + @Min(2000) + @Max(2100) private Integer year; + @NotNull private 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 index bde33421a..5f2958660 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java +++ b/src/main/java/com/wcc/platform/repository/postgres/PostgresFeedbackRepository.java @@ -8,7 +8,6 @@ import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Primary; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; @@ -18,7 +17,6 @@ * the result sets to Feedback objects with the help of FeedbackRowMapper. */ @Repository -@Primary @RequiredArgsConstructor public class PostgresFeedbackRepository implements FeedbackRepository { 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 index 29f2e3d80..af27fe64c 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/component/FeedbackMapper.java +++ b/src/main/java/com/wcc/platform/repository/postgres/component/FeedbackMapper.java @@ -1,19 +1,19 @@ package com.wcc.platform.repository.postgres.component; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_CREATED_AT; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_FEEDBACK_TEXT; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_FEEDBACK_TYPE_ID; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_ID; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_IS_ANONYMOUS; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_IS_APPROVED; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_MENTORSHIP_CYCLE_ID; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_RATING; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWEE_ID; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWEE_NAME; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWER_ID; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWER_NAME; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_UPDATED_AT; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_YEAR; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_CREATED_AT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_FEEDBACK_TEXT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_FB_TYPE_ID; +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; @@ -50,29 +50,28 @@ public class FeedbackMapper { /** Maps a database row to a Feedback object. */ public Feedback mapRowToFeedback(final ResultSet rs) throws SQLException { return Feedback.builder() - .id(rs.getLong(COLUMN_ID)) - .reviewerId(rs.getLong(COLUMN_REVIEWER_ID)) - .reviewerName(rs.getString(COLUMN_REVIEWER_NAME)) - .revieweeId( - rs.getObject(COLUMN_REVIEWEE_ID) != null ? rs.getLong(COLUMN_REVIEWEE_ID) : null) - .revieweeName(rs.getString(COLUMN_REVIEWEE_NAME)) + .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(COLUMN_MENTORSHIP_CYCLE_ID) != null - ? rs.getLong(COLUMN_MENTORSHIP_CYCLE_ID) + rs.getObject(COL_MS_CYCLE_ID) != null + ? rs.getLong(COL_MS_CYCLE_ID) : null) - .feedbackType(FeedbackType.fromId(rs.getInt(COLUMN_FEEDBACK_TYPE_ID))) - .rating(rs.getObject(COLUMN_RATING) != null ? rs.getInt(COLUMN_RATING) : null) - .feedbackText(rs.getString(COLUMN_FEEDBACK_TEXT)) - .year(rs.getObject(COLUMN_YEAR) != null ? rs.getInt(COLUMN_YEAR) : null) - .isAnonymous(rs.getBoolean(COLUMN_IS_ANONYMOUS)) - .isApproved(rs.getBoolean(COLUMN_IS_APPROVED)) + .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(COLUMN_CREATED_AT) != null - ? rs.getObject(COLUMN_CREATED_AT, OffsetDateTime.class) + rs.getObject(COL_CREATED_AT) != null + ? rs.getObject(COL_CREATED_AT, OffsetDateTime.class) : null) .updatedAt( - rs.getObject(COLUMN_UPDATED_AT) != null - ? rs.getObject(COLUMN_UPDATED_AT, OffsetDateTime.class) + rs.getObject(COL_UPDATED_AT) != null + ? rs.getObject(COL_UPDATED_AT, OffsetDateTime.class) : null) .build(); } 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 index 7005db465..3bc4a9437 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/constants/FeedbackConstants.java +++ b/src/main/java/com/wcc/platform/repository/postgres/constants/FeedbackConstants.java @@ -1,24 +1,24 @@ package com.wcc.platform.repository.postgres.constants; /** Constants related to Feedback entity. */ -@SuppressWarnings({"PMD.DataClass", "PMD.LongVariable"}) +@SuppressWarnings("PMD.DataClass") public final class FeedbackConstants { public static final String TABLE = "feedback"; - public static final String COLUMN_ID = "id"; - public static final String COLUMN_REVIEWER_ID = "reviewer_id"; - public static final String COLUMN_REVIEWEE_ID = "reviewee_id"; - public static final String COLUMN_REVIEWER_NAME = "reviewer_name"; - public static final String COLUMN_REVIEWEE_NAME = "reviewee_name"; - public static final String COLUMN_MENTORSHIP_CYCLE_ID = "mentorship_cycle_id"; - public static final String COLUMN_FEEDBACK_TYPE_ID = "feedback_type_id"; - public static final String COLUMN_RATING = "rating"; - public static final String COLUMN_FEEDBACK_TEXT = "feedback_text"; - public static final String COLUMN_YEAR = "feedback_year"; - public static final String COLUMN_IS_ANONYMOUS = "is_anonymous"; - public static final String COLUMN_IS_APPROVED = "is_approved"; - public static final String COLUMN_CREATED_AT = "created_at"; - public static final String COLUMN_UPDATED_AT = "updated_at"; + 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/test/java/com/wcc/platform/domain/platform/feedback/validation/FeedbackDtoValidationTest.java b/src/test/java/com/wcc/platform/domain/platform/feedback/validation/FeedbackDtoValidationTest.java index f934e8376..f31d9d1e8 100644 --- 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 @@ -249,4 +249,63 @@ void testBlankFeedbackTextFails() { 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/repository/postgres/component/FeedbackMapperTest.java b/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java index fef3f14b2..5ed43b633 100644 --- a/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java +++ b/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java @@ -2,20 +2,20 @@ 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.COLUMN_CREATED_AT; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_FEEDBACK_TEXT; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_FEEDBACK_TYPE_ID; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_ID; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_IS_ANONYMOUS; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_IS_APPROVED; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_MENTORSHIP_CYCLE_ID; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_RATING; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWEE_ID; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWEE_NAME; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWER_ID; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_REVIEWER_NAME; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_UPDATED_AT; -import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COLUMN_YEAR; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_CREATED_AT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_FEEDBACK_TEXT; +import static com.wcc.platform.repository.postgres.constants.FeedbackConstants.COL_FB_TYPE_ID; +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; @@ -67,26 +67,26 @@ void testMapRowToFeedback() throws SQLException { OffsetDateTime createdAt = OffsetDateTime.now(); OffsetDateTime updatedAt = OffsetDateTime.now(); - when(resultSet.getLong(COLUMN_ID)).thenReturn(feedbackId); - when(resultSet.getLong(COLUMN_REVIEWER_ID)).thenReturn(reviewerId); - when(resultSet.getString(COLUMN_REVIEWER_NAME)).thenReturn("Reviewer Name"); - when(resultSet.getObject(COLUMN_REVIEWEE_ID)).thenReturn(revieweeId); - when(resultSet.getLong(COLUMN_REVIEWEE_ID)).thenReturn(revieweeId); - when(resultSet.getString(COLUMN_REVIEWEE_NAME)).thenReturn("Reviewee Name"); - when(resultSet.getObject(COLUMN_MENTORSHIP_CYCLE_ID)).thenReturn(mentorshipCycleId); - when(resultSet.getLong(COLUMN_MENTORSHIP_CYCLE_ID)).thenReturn(mentorshipCycleId); - when(resultSet.getInt(COLUMN_FEEDBACK_TYPE_ID)).thenReturn(feedbackTypeId); - when(resultSet.getObject(COLUMN_RATING)).thenReturn(rating); - when(resultSet.getInt(COLUMN_RATING)).thenReturn(rating); - when(resultSet.getString(COLUMN_FEEDBACK_TEXT)).thenReturn(feedbackText); - when(resultSet.getObject(COLUMN_YEAR)).thenReturn(year); - when(resultSet.getInt(COLUMN_YEAR)).thenReturn(year); - when(resultSet.getBoolean(COLUMN_IS_ANONYMOUS)).thenReturn(isAnonymous); - when(resultSet.getBoolean(COLUMN_IS_APPROVED)).thenReturn(isApproved); - when(resultSet.getObject(COLUMN_CREATED_AT)).thenReturn(createdAt); - when(resultSet.getObject(COLUMN_CREATED_AT, OffsetDateTime.class)).thenReturn(createdAt); - when(resultSet.getObject(COLUMN_UPDATED_AT)).thenReturn(updatedAt); - when(resultSet.getObject(COLUMN_UPDATED_AT, OffsetDateTime.class)).thenReturn(updatedAt); + 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); @@ -119,20 +119,20 @@ void testMapRowToFeedbackWithNullableFields() throws SQLException { Boolean isAnonymous = true; Boolean isApproved = false; - when(resultSet.getLong(COLUMN_ID)).thenReturn(feedbackId); - when(resultSet.getLong(COLUMN_REVIEWER_ID)).thenReturn(reviewerId); - when(resultSet.getString(COLUMN_REVIEWER_NAME)).thenReturn("Reviewer Name"); - when(resultSet.getObject(COLUMN_REVIEWEE_ID)).thenReturn(null); - when(resultSet.getString(COLUMN_REVIEWEE_NAME)).thenReturn(null); - when(resultSet.getObject(COLUMN_MENTORSHIP_CYCLE_ID)).thenReturn(null); - when(resultSet.getInt(COLUMN_FEEDBACK_TYPE_ID)).thenReturn(feedbackTypeId); - when(resultSet.getObject(COLUMN_RATING)).thenReturn(null); - when(resultSet.getString(COLUMN_FEEDBACK_TEXT)).thenReturn(feedbackText); - when(resultSet.getObject(COLUMN_YEAR)).thenReturn(null); - when(resultSet.getBoolean(COLUMN_IS_ANONYMOUS)).thenReturn(isAnonymous); - when(resultSet.getBoolean(COLUMN_IS_APPROVED)).thenReturn(isApproved); - when(resultSet.getObject(COLUMN_CREATED_AT)).thenReturn(null); - when(resultSet.getObject(COLUMN_UPDATED_AT)).thenReturn(null); + 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); @@ -154,7 +154,7 @@ void testMapRowToFeedbackWithNullableFields() throws SQLException { @Test @DisplayName("Given ResultSet throws SQLException, when mapping, then propagates exception") void handlesSqlExceptionGracefully() throws Exception { - when(resultSet.getLong(COLUMN_ID)).thenThrow(SQLException.class); + when(resultSet.getLong(COL_ID)).thenThrow(SQLException.class); assertThrows(SQLException.class, () -> feedbackMapper.mapRowToFeedback(resultSet)); } From 80f682a9e5ae3642cf36ecdef02b2c573bb0d109 Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Tue, 5 May 2026 22:21:44 +0200 Subject: [PATCH 16/17] feat: Feedback - small updates - fix PMD warnings --- .../repository/postgres/component/FeedbackMapper.java | 6 ++---- .../repository/postgres/component/FeedbackMapperTest.java | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) 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 index af27fe64c..6a10b22d8 100644 --- a/src/main/java/com/wcc/platform/repository/postgres/component/FeedbackMapper.java +++ b/src/main/java/com/wcc/platform/repository/postgres/component/FeedbackMapper.java @@ -1,8 +1,8 @@ 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_FEEDBACK_TEXT; 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; @@ -56,9 +56,7 @@ public Feedback mapRowToFeedback(final ResultSet rs) throws SQLException { .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) + 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)) 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 index 5ed43b633..515f81a8d 100644 --- a/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java +++ b/src/test/java/com/wcc/platform/repository/postgres/component/FeedbackMapperTest.java @@ -3,8 +3,8 @@ 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_FEEDBACK_TEXT; 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; From c9804817a8e2d7fcc9c2063c88a5d114bdd3bf27 Mon Sep 17 00:00:00 2001 From: Nevena Verbic Date: Thu, 7 May 2026 18:26:55 +0200 Subject: [PATCH 17/17] Revert package.json changes --- admin-wcc-app/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/admin-wcc-app/package.json b/admin-wcc-app/package.json index 3e08a9843..a00955253 100644 --- a/admin-wcc-app/package.json +++ b/admin-wcc-app/package.json @@ -3,9 +3,9 @@ "private": true, "version": "0.1.0", "scripts": { - "dev": "next dev -p 3001", + "dev": "next dev -p ${PORT:-3000}", "build": "next build", - "start": "next start -p 3001", + "start": "next start -p ${PORT:-3000}", "lint": "next lint", "test": "jest", "format": "prettier --write .",