diff --git a/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/controller/WorkplaceReviewController.java b/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/controller/WorkplaceReviewController.java index 5541ab3b..de6a9692 100644 --- a/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/controller/WorkplaceReviewController.java +++ b/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/controller/WorkplaceReviewController.java @@ -23,9 +23,11 @@ public class WorkplaceReviewController { private User currentUser() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth == null || !auth.isAuthenticated()) throw new AccessDeniedException("Unauthenticated"); + if (auth == null || !auth.isAuthenticated()) + throw new AccessDeniedException("Unauthenticated"); Object principal = auth.getPrincipal(); - if (principal instanceof User u) return u; + if (principal instanceof User u) + return u; if (principal instanceof UserDetails ud) { String key = ud.getUsername(); return userRepository.findByEmail(key).or(() -> userRepository.findByUsername(key)) @@ -39,44 +41,52 @@ private User currentUser() { // === REVIEWS === @PostMapping("/review") public ResponseEntity create(@PathVariable Long workplaceId, - @RequestBody @Valid ReviewCreateRequest req) { + @RequestBody @Valid ReviewCreateRequest req) { var res = reviewService.createReview(workplaceId, req, currentUser()); return ResponseEntity.ok(res); } @GetMapping("/review") public ResponseEntity> list(@PathVariable Long workplaceId, - @RequestParam(defaultValue = "0") int page, - @RequestParam(defaultValue = "10") int size, - @RequestParam(required = false) String ratingFilter, - @RequestParam(required = false) String sortBy, - @RequestParam(required = false) Boolean hasComment, - @RequestParam(required = false) String policy, - @RequestParam(required = false) Integer policyMin) { - var res = reviewService.listReviews(workplaceId, page, size, ratingFilter, sortBy, hasComment, policy, policyMin); + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "10") int size, + @RequestParam(required = false) String ratingFilter, + @RequestParam(required = false) String sortBy, + @RequestParam(required = false) Boolean hasComment, + @RequestParam(required = false) String policy, + @RequestParam(required = false) Integer policyMin) { + var res = reviewService.listReviews(workplaceId, page, size, ratingFilter, sortBy, hasComment, policy, + policyMin, currentUser()); return ResponseEntity.ok(res); } @GetMapping("/review/{reviewId}") public ResponseEntity getOne(@PathVariable Long workplaceId, - @PathVariable Long reviewId) { - return ResponseEntity.ok(reviewService.getOne(workplaceId, reviewId)); + @PathVariable Long reviewId) { + return ResponseEntity.ok(reviewService.getOne(workplaceId, reviewId, currentUser())); } @PutMapping("/review/{reviewId}") public ResponseEntity update(@PathVariable Long workplaceId, - @PathVariable Long reviewId, - @RequestBody @Valid ReviewUpdateRequest req) { + @PathVariable Long reviewId, + @RequestBody @Valid ReviewUpdateRequest req) { var res = reviewService.updateReview(workplaceId, reviewId, req, currentUser()); return ResponseEntity.ok(res); } @DeleteMapping("/review/{reviewId}") public ResponseEntity delete(@PathVariable Long workplaceId, - @PathVariable Long reviewId) { + @PathVariable Long reviewId) { boolean isAdmin = false; // TODO: implement isAdmin check in milestone 3 reviewService.deleteReview(workplaceId, reviewId, currentUser(), isAdmin); return ResponseEntity.ok(ApiMessage.builder().message("Review deleted").code("REVIEW_DELETED").build()); } + @PostMapping("/review/{reviewId}/helpful") + public ResponseEntity toggleHelpful(@PathVariable Long workplaceId, + @PathVariable Long reviewId) { + var res = reviewService.toggleHelpful(workplaceId, reviewId, currentUser()); + return ResponseEntity.ok(res); + } + } diff --git a/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/dto/ReviewResponse.java b/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/dto/ReviewResponse.java index a169c6ab..5974ea35 100644 --- a/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/dto/ReviewResponse.java +++ b/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/dto/ReviewResponse.java @@ -23,6 +23,7 @@ public class ReviewResponse { private Double overallRating; private Map ethicalPolicyRatings; private ReplyResponse reply; + private boolean isHelpfulByUser; private Instant createdAt; private Instant updatedAt; } \ No newline at end of file diff --git a/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/model/ReviewReaction.java b/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/model/ReviewReaction.java new file mode 100644 index 00000000..7ecf2ff5 --- /dev/null +++ b/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/model/ReviewReaction.java @@ -0,0 +1,35 @@ +package org.bounswe.jobboardbackend.workplace.model; + +import jakarta.persistence.*; +import lombok.*; +import org.bounswe.jobboardbackend.auth.model.User; +import org.hibernate.annotations.CreationTimestamp; + +import java.time.Instant; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +@Table(uniqueConstraints = { + @UniqueConstraint(columnNames = { "review_id", "user_id" }) +}) +public class ReviewReaction { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "review_id") + private Review review; + + @ManyToOne(fetch = FetchType.LAZY, optional = false) + @JoinColumn(name = "user_id") + private User user; + + @CreationTimestamp + @Column(nullable = false, updatable = false) + private Instant createdAt; +} diff --git a/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/repository/ReviewReactionRepository.java b/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/repository/ReviewReactionRepository.java new file mode 100644 index 00000000..5412ce94 --- /dev/null +++ b/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/repository/ReviewReactionRepository.java @@ -0,0 +1,15 @@ +package org.bounswe.jobboardbackend.workplace.repository; + +import org.bounswe.jobboardbackend.workplace.model.ReviewReaction; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface ReviewReactionRepository extends JpaRepository { + boolean existsByReview_IdAndUser_Id(Long reviewId, Long userId); + + Optional findByReview_IdAndUser_Id(Long reviewId, Long userId); + + List findByUser_IdAndReview_IdIn(Long userId, List reviewIds); +} diff --git a/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/service/ReviewService.java b/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/service/ReviewService.java index 64b17cee..d3a4c516 100644 --- a/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/service/ReviewService.java +++ b/apps/jobboard-backend/src/main/java/org/bounswe/jobboardbackend/workplace/service/ReviewService.java @@ -24,372 +24,428 @@ @RequiredArgsConstructor public class ReviewService { - private final WorkplaceRepository workplaceRepository; - private final ReviewRepository reviewRepository; - private final ReviewPolicyRatingRepository reviewPolicyRatingRepository; - private final ReviewReplyRepository reviewReplyRepository; - private final EmployerWorkplaceRepository employerWorkplaceRepository; - private final UserRepository userRepository; - private final ProfileRepository profileRepository; - - // === CREATE REVIEW === - @Transactional - public ReviewResponse createReview(Long workplaceId, ReviewCreateRequest req, User currentUser) { - Workplace wp = workplaceRepository.findById(workplaceId) - .orElseThrow(() -> new HandleException( - ErrorCode.WORKPLACE_NOT_FOUND, - "Workplace not found" - )); - - boolean isEmployer = employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, currentUser.getId()); - if (isEmployer) { - throw new HandleException( - ErrorCode.WORKPLACE_UNAUTHORIZED, - "Employers cannot review their own workplace" - ); - } + private final WorkplaceRepository workplaceRepository; + private final ReviewRepository reviewRepository; + private final ReviewPolicyRatingRepository reviewPolicyRatingRepository; + private final ReviewReplyRepository reviewReplyRepository; + private final EmployerWorkplaceRepository employerWorkplaceRepository; + private final UserRepository userRepository; + private final ProfileRepository profileRepository; + private final ReviewReactionRepository reviewReactionRepository; + + // === CREATE REVIEW === + @Transactional + public ReviewResponse createReview(Long workplaceId, ReviewCreateRequest req, User currentUser) { + Workplace wp = workplaceRepository.findById(workplaceId) + .orElseThrow(() -> new HandleException( + ErrorCode.WORKPLACE_NOT_FOUND, + "Workplace not found")); + + boolean isEmployer = employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, + currentUser.getId()); + if (isEmployer) { + throw new HandleException( + ErrorCode.WORKPLACE_UNAUTHORIZED, + "Employers cannot review their own workplace"); + } - boolean alreadyReviewed = reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, currentUser.getId()); - if (alreadyReviewed) { - throw new HandleException( - ErrorCode.REVIEW_ALREADY_EXISTS, - "You have already submitted a review for this workplace." - ); - } + boolean alreadyReviewed = reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, + currentUser.getId()); + if (alreadyReviewed) { + throw new HandleException( + ErrorCode.REVIEW_ALREADY_EXISTS, + "You have already submitted a review for this workplace."); + } - currentUser = userRepository.findById(currentUser.getId()) - .orElseThrow(() -> new HandleException( - ErrorCode.USER_NOT_FOUND, - "User not found" - )); - - Set allowed = wp.getEthicalTags(); - if (allowed == null || allowed.isEmpty()) { - throw new HandleException( - ErrorCode.VALIDATION_ERROR, - "This workplace has no declared ethical tags to rate." - ); - } + currentUser = userRepository.findById(currentUser.getId()) + .orElseThrow(() -> new HandleException( + ErrorCode.USER_NOT_FOUND, + "User not found")); - Map policyMap = req.getEthicalPolicyRatings(); - if (policyMap == null || policyMap.isEmpty()) { - throw new HandleException( - ErrorCode.VALIDATION_ERROR, - "ethicalPolicyRatings must contain at least one policy rating (1..5)" - ); - } + Set allowed = wp.getEthicalTags(); + if (allowed == null || allowed.isEmpty()) { + throw new HandleException( + ErrorCode.VALIDATION_ERROR, + "This workplace has no declared ethical tags to rate."); + } - List> validated = new ArrayList<>(); - for (Map.Entry e : policyMap.entrySet()) { - String key = e.getKey(); - Integer score = e.getValue(); - - if (score == null || score < 1 || score > 5) { - throw new HandleException( - ErrorCode.VALIDATION_ERROR, - "Score for '" + key + "' must be between 1 and 5." - ); - } - - EthicalPolicy policy; - try { - policy = EthicalPolicy.fromLabel(key); - } catch (IllegalArgumentException ex) { - throw new HandleException( - ErrorCode.VALIDATION_ERROR, - "Unknown ethical policy: " + key - ); - } - - if (!allowed.contains(policy)) { - throw new HandleException( - ErrorCode.VALIDATION_ERROR, - "Policy '" + policy.name() + "' is not declared by this workplace." - ); - } - - validated.add(Map.entry(policy, score)); - } + Map policyMap = req.getEthicalPolicyRatings(); + if (policyMap == null || policyMap.isEmpty()) { + throw new HandleException( + ErrorCode.VALIDATION_ERROR, + "ethicalPolicyRatings must contain at least one policy rating (1..5)"); + } - Review review = Review.builder() - .workplace(wp) - .user(currentUser) - .title(req.getTitle()) - .content(req.getContent()) - .anonymous(req.isAnonymous()) - .helpfulCount(0) - .build(); - review = reviewRepository.save(review); - - for (Map.Entry e : validated) { - ReviewPolicyRating rpr = ReviewPolicyRating.builder() - .review(review) - .policy(e.getKey()) - .score(e.getValue()) - .build(); - reviewPolicyRatingRepository.save(rpr); - } + List> validated = new ArrayList<>(); + for (Map.Entry e : policyMap.entrySet()) { + String key = e.getKey(); + Integer score = e.getValue(); + + if (score == null || score < 1 || score > 5) { + throw new HandleException( + ErrorCode.VALIDATION_ERROR, + "Score for '" + key + "' must be between 1 and 5."); + } + + EthicalPolicy policy; + try { + policy = EthicalPolicy.fromLabel(key); + } catch (IllegalArgumentException ex) { + throw new HandleException( + ErrorCode.VALIDATION_ERROR, + "Unknown ethical policy: " + key); + } + + if (!allowed.contains(policy)) { + throw new HandleException( + ErrorCode.VALIDATION_ERROR, + "Policy '" + policy.name() + "' is not declared by this workplace."); + } + + validated.add(Map.entry(policy, score)); + } - double avg = validated.stream().mapToInt(Map.Entry::getValue).average().orElse(0.0); - double overall = Math.round(Math.max(1.0, Math.min(5.0, avg)) * 10.0) / 10.0; - review.setOverallRating(overall); - reviewRepository.save(review); - - wp.setReviewCount(wp.getReviewCount() + 1); - workplaceRepository.save(wp); - - return toResponse(review, true); - } - - // === LIST REVIEWS === - @Transactional(readOnly = true) - public PaginatedResponse listReviews(Long workplaceId, Integer page, Integer size, - String ratingFilter, String sortBy, Boolean hasComment, - String policy, Integer policyMin) { - Workplace wp = workplaceRepository.findById(workplaceId) - .orElseThrow(() -> new HandleException( - ErrorCode.WORKPLACE_NOT_FOUND, - "Workplace not found" - )); - - Pageable pageable = makeSort(page, size, sortBy); - - Page pg; - if (ratingFilter != null && !ratingFilter.isBlank()) { - List values = Arrays.stream(ratingFilter.split(",")) - .map(String::trim) - .filter(s -> !s.isEmpty()) - .map(Double::parseDouble) - .map(d -> Math.max(1.0, Math.min(5.0, d))) - .sorted() - .toList(); - - if (values.size() == 1) { - pg = reviewRepository.findByWorkplace_IdAndOverallRatingIn(workplaceId, values, pageable); - } else { - Double min = values.getFirst(); - Double max = values.getLast(); - pg = reviewRepository.findByWorkplace_IdAndOverallRatingBetween(workplaceId, min, max, pageable); - } - } else if (Boolean.TRUE.equals(hasComment)) { - pg = reviewRepository.findByWorkplace_IdAndContentIsNotNullAndContentNot(workplaceId, "", pageable); - } else { - pg = reviewRepository.findByWorkplace_Id(workplaceId, pageable); - } + Review review = Review.builder() + .workplace(wp) + .user(currentUser) + .title(req.getTitle()) + .content(req.getContent()) + .anonymous(req.isAnonymous()) + .helpfulCount(0) + .build(); + review = reviewRepository.save(review); + + for (Map.Entry e : validated) { + ReviewPolicyRating rpr = ReviewPolicyRating.builder() + .review(review) + .policy(e.getKey()) + .score(e.getValue()) + .build(); + reviewPolicyRatingRepository.save(rpr); + } + + double avg = validated.stream().mapToInt(Map.Entry::getValue).average().orElse(0.0); + double overall = Math.round(Math.max(1.0, Math.min(5.0, avg)) * 10.0) / 10.0; + review.setOverallRating(overall); + reviewRepository.save(review); + + wp.setReviewCount(wp.getReviewCount() + 1); + workplaceRepository.save(wp); - List content = pg.getContent().stream() - .map(r -> toResponse(r, true)) - .collect(Collectors.toList()); - - return PaginatedResponse.of(content, pg.getNumber(), pg.getSize(), pg.getTotalElements()); - } - - // === GET ONE === - @Transactional(readOnly = true) - public ReviewResponse getOne(Long workplaceId, Long reviewId) { - Review r = reviewRepository.findById(reviewId) - .orElseThrow(() -> new HandleException( - ErrorCode.REVIEW_NOT_FOUND, - "Review not found" - )); - if (!Objects.equals(r.getWorkplace().getId(), workplaceId)) { - throw new HandleException( - ErrorCode.REVIEW_NOT_FOUND, - "Review does not belong to workplace" - ); + return toResponse(review, true, currentUser); } - return toResponse(r, true); - } - - // === UPDATE REVIEW === - @Transactional - public ReviewResponse updateReview(Long workplaceId, Long reviewId, ReviewUpdateRequest req, User currentUser) { - Review r = reviewRepository.findById(reviewId) - .orElseThrow(() -> new HandleException( - ErrorCode.REVIEW_NOT_FOUND, - "Review not found" - )); - if (!Objects.equals(r.getWorkplace().getId(), workplaceId)) { - throw new HandleException( - ErrorCode.REVIEW_NOT_FOUND, - "Review does not belong to workplace" - ); + + // === LIST REVIEWS === + @Transactional(readOnly = true) + public PaginatedResponse listReviews(Long workplaceId, Integer page, Integer size, + String ratingFilter, String sortBy, Boolean hasComment, + String policy, Integer policyMin, User currentUser) { + workplaceRepository.findById(workplaceId) + .orElseThrow(() -> new HandleException( + ErrorCode.WORKPLACE_NOT_FOUND, + "Workplace not found")); + + Pageable pageable = makeSort(page, size, sortBy); + + Page pg; + if (ratingFilter != null && !ratingFilter.isBlank()) { + List values = Arrays.stream(ratingFilter.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Double::parseDouble) + .map(d -> Math.max(1.0, Math.min(5.0, d))) + .sorted() + .toList(); + + if (values.size() == 1) { + pg = reviewRepository.findByWorkplace_IdAndOverallRatingIn(workplaceId, values, + pageable); + } else { + Double min = values.getFirst(); + Double max = values.getLast(); + pg = reviewRepository.findByWorkplace_IdAndOverallRatingBetween(workplaceId, min, max, + pageable); + } + } else if (Boolean.TRUE.equals(hasComment)) { + pg = reviewRepository.findByWorkplace_IdAndContentIsNotNullAndContentNot(workplaceId, "", + pageable); + } else { + pg = reviewRepository.findByWorkplace_Id(workplaceId, pageable); + } + + List reviews = pg.getContent(); + Set likedReviewIds = new HashSet<>(); + if (currentUser != null) { + List reviewIds = reviews.stream().map(Review::getId).toList(); + if (!reviewIds.isEmpty()) { + likedReviewIds = reviewReactionRepository + .findByUser_IdAndReview_IdIn(currentUser.getId(), reviewIds) + .stream() + .map(rr -> rr.getReview().getId()) + .collect(Collectors.toSet()); + } + } + + Set finalLikedReviewIds = likedReviewIds; + List content = reviews.stream() + .map(r -> toResponse(r, true, finalLikedReviewIds.contains(r.getId()))) + .collect(Collectors.toList()); + + return PaginatedResponse.of(content, pg.getNumber(), pg.getSize(), pg.getTotalElements()); } - if (!Objects.equals(r.getUser().getId(), currentUser.getId())) { - throw new HandleException( - ErrorCode.ACCESS_DENIED, - "Only owner can edit the review" - ); + + // === GET ONE === + @Transactional(readOnly = true) + public ReviewResponse getOne(Long workplaceId, Long reviewId, User currentUser) { + Review r = reviewRepository.findById(reviewId) + .orElseThrow(() -> new HandleException( + ErrorCode.REVIEW_NOT_FOUND, + "Review not found")); + if (!Objects.equals(r.getWorkplace().getId(), workplaceId)) { + throw new HandleException( + ErrorCode.REVIEW_NOT_FOUND, + "Review does not belong to workplace"); + } + return toResponse(r, true, currentUser); } - if (req.getTitle() != null) r.setTitle(req.getTitle()); - if (req.getContent() != null) r.setContent(req.getContent()); - if (req.getIsAnonymous() != null) r.setAnonymous(req.getIsAnonymous()); - reviewRepository.save(r); - - if (req.getEthicalPolicyRatings() != null) { - Set allowed = r.getWorkplace().getEthicalTags(); - if (allowed == null || allowed.isEmpty()) { - throw new HandleException( - ErrorCode.VALIDATION_ERROR, - "This workplace has no declared ethical tags to rate." - ); - } - - List existing = reviewPolicyRatingRepository.findByReview_Id(r.getId()); - Map byPolicy = existing.stream() - .collect(Collectors.toMap(ReviewPolicyRating::getPolicy, x -> x)); - - for (Map.Entry e : req.getEthicalPolicyRatings().entrySet()) { - String key = e.getKey(); - Integer score = e.getValue(); - - if (score == null || score < 1 || score > 5) { - throw new HandleException( - ErrorCode.VALIDATION_ERROR, - "Score for '" + key + "' must be between 1 and 5." - ); + // === UPDATE REVIEW === + @Transactional + public ReviewResponse updateReview(Long workplaceId, Long reviewId, ReviewUpdateRequest req, User currentUser) { + Review r = reviewRepository.findById(reviewId) + .orElseThrow(() -> new HandleException( + ErrorCode.REVIEW_NOT_FOUND, + "Review not found")); + if (!Objects.equals(r.getWorkplace().getId(), workplaceId)) { + throw new HandleException( + ErrorCode.REVIEW_NOT_FOUND, + "Review does not belong to workplace"); + } + if (!Objects.equals(r.getUser().getId(), currentUser.getId())) { + throw new HandleException( + ErrorCode.ACCESS_DENIED, + "Only owner can edit the review"); } - EthicalPolicy policy; - try { - policy = EthicalPolicy.fromLabel(key); - } catch (IllegalArgumentException ex) { - throw new HandleException( - ErrorCode.VALIDATION_ERROR, - "Unknown ethical policy: " + key - ); + if (req.getTitle() != null) + r.setTitle(req.getTitle()); + if (req.getContent() != null) + r.setContent(req.getContent()); + if (req.getIsAnonymous() != null) + r.setAnonymous(req.getIsAnonymous()); + reviewRepository.save(r); + + if (req.getEthicalPolicyRatings() != null) { + Set allowed = r.getWorkplace().getEthicalTags(); + if (allowed == null || allowed.isEmpty()) { + throw new HandleException( + ErrorCode.VALIDATION_ERROR, + "This workplace has no declared ethical tags to rate."); + } + + List existing = reviewPolicyRatingRepository.findByReview_Id(r.getId()); + Map byPolicy = existing.stream() + .collect(Collectors.toMap(ReviewPolicyRating::getPolicy, x -> x)); + + for (Map.Entry e : req.getEthicalPolicyRatings().entrySet()) { + String key = e.getKey(); + Integer score = e.getValue(); + + if (score == null || score < 1 || score > 5) { + throw new HandleException( + ErrorCode.VALIDATION_ERROR, + "Score for '" + key + "' must be between 1 and 5."); + } + + EthicalPolicy policy; + try { + policy = EthicalPolicy.fromLabel(key); + } catch (IllegalArgumentException ex) { + throw new HandleException( + ErrorCode.VALIDATION_ERROR, + "Unknown ethical policy: " + key); + } + + if (!allowed.contains(policy)) { + throw new HandleException( + ErrorCode.VALIDATION_ERROR, + "Policy '" + policy.name() + + "' is not declared by this workplace."); + } + + ReviewPolicyRating entity = byPolicy.get(policy); + if (entity != null) { + // UPDATE + entity.setScore(score); + } else { + // INSERT + ReviewPolicyRating created = ReviewPolicyRating.builder() + .review(r) + .policy(policy) + .score(score) + .build(); + reviewPolicyRatingRepository.save(created); + byPolicy.put(policy, created); + } + } + + List current = reviewPolicyRatingRepository.findByReview_Id(r.getId()); + if (!current.isEmpty()) { + double avg = current.stream().mapToInt(ReviewPolicyRating::getScore).average() + .orElse(0.0); + double overall = Math.round(Math.max(1.0, Math.min(5.0, avg)) * 10.0) / 10.0; + r.setOverallRating(overall); + } + reviewRepository.save(r); } - if (!allowed.contains(policy)) { - throw new HandleException( - ErrorCode.VALIDATION_ERROR, - "Policy '" + policy.name() + "' is not declared by this workplace." - ); + return toResponse(r, true, currentUser); + } + + // === TOGGLE HELPFUL === + @Transactional + public ReviewResponse toggleHelpful(Long workplaceId, Long reviewId, User currentUser) { + Review r = reviewRepository.findById(reviewId) + .orElseThrow(() -> new HandleException( + ErrorCode.REVIEW_NOT_FOUND, + "Review not found")); + + if (!Objects.equals(r.getWorkplace().getId(), workplaceId)) { + throw new HandleException( + ErrorCode.REVIEW_NOT_FOUND, + "Review does not belong to workplace"); + } + + if (Objects.equals(r.getUser().getId(), currentUser.getId())) { + throw new HandleException( + ErrorCode.VALIDATION_ERROR, + "You cannot mark your own review as helpful."); } - ReviewPolicyRating entity = byPolicy.get(policy); - if (entity != null) { - // UPDATE - entity.setScore(score); + Optional reaction = reviewReactionRepository.findByReview_IdAndUser_Id(reviewId, + currentUser.getId()); + boolean isHelpful; + + if (reaction.isPresent()) { + // Unlike + reviewReactionRepository.delete(reaction.get()); + r.setHelpfulCount(Math.max(0, r.getHelpfulCount() - 1)); + isHelpful = false; } else { - // INSERT - ReviewPolicyRating created = ReviewPolicyRating.builder() - .review(r) - .policy(policy) - .score(score) - .build(); - reviewPolicyRatingRepository.save(created); - byPolicy.put(policy, created); + // Like + ReviewReaction newReaction = ReviewReaction.builder() + .review(r) + .user(currentUser) + .build(); + reviewReactionRepository.save(newReaction); + r.setHelpfulCount(r.getHelpfulCount() + 1); + isHelpful = true; } - } - List current = reviewPolicyRatingRepository.findByReview_Id(r.getId()); - if (!current.isEmpty()) { - double avg = current.stream().mapToInt(ReviewPolicyRating::getScore).average().orElse(0.0); - double overall = Math.round(Math.max(1.0, Math.min(5.0, avg)) * 10.0) / 10.0; - r.setOverallRating(overall); - } - reviewRepository.save(r); + reviewRepository.save(r); + return toResponse(r, true, isHelpful); } - return toResponse(r, true); - } - - // === DELETE REVIEW === - @Transactional - public void deleteReview(Long workplaceId, Long reviewId, User currentUser, boolean isAdmin) { - Review r = reviewRepository.findById(reviewId) - .orElseThrow(() -> new HandleException( - ErrorCode.REVIEW_NOT_FOUND, - "Review not found" - )); - if (!Objects.equals(r.getWorkplace().getId(), workplaceId)) { - throw new HandleException( - ErrorCode.REVIEW_NOT_FOUND, - "Review does not belong to workplace" - ); + // === DELETE REVIEW === + @Transactional + public void deleteReview(Long workplaceId, Long reviewId, User currentUser, boolean isAdmin) { + Review r = reviewRepository.findById(reviewId) + .orElseThrow(() -> new HandleException( + ErrorCode.REVIEW_NOT_FOUND, + "Review not found")); + if (!Objects.equals(r.getWorkplace().getId(), workplaceId)) { + throw new HandleException( + ErrorCode.REVIEW_NOT_FOUND, + "Review does not belong to workplace"); + } + boolean reviewOwner = Objects.equals(r.getUser().getId(), currentUser.getId()); + if (!(reviewOwner || isAdmin)) { + throw new HandleException( + ErrorCode.ACCESS_DENIED, + "Only review owner or admin can delete the review"); + } + + reviewReplyRepository.findByReview_Id(reviewId).ifPresent(reviewReplyRepository::delete); + reviewPolicyRatingRepository.deleteAll(reviewPolicyRatingRepository.findByReview_Id(reviewId)); + reviewRepository.delete(r); + + Workplace wp = workplaceRepository.findById(workplaceId) + .orElseThrow(() -> new HandleException( + ErrorCode.WORKPLACE_NOT_FOUND, + "Workplace not found")); + long currentCount = wp.getReviewCount(); + wp.setReviewCount(Math.max(0, currentCount - 1)); + workplaceRepository.save(wp); } - boolean reviewOwner = Objects.equals(r.getUser().getId(), currentUser.getId()); - if (!(reviewOwner || isAdmin)) { - throw new HandleException( - ErrorCode.ACCESS_DENIED, - "Only review owner or admin can delete the review" - ); + + // === HELPERS === + private Pageable makeSort(int page, int size, String sortBy) { + if (sortBy == null) { + return PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + } + return switch (sortBy) { + case "ratingAsc" -> PageRequest.of(page, size, Sort.by(Sort.Direction.ASC, "overallRating")); + case "ratingDesc" -> PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "overallRating")); + case "helpfulness" -> PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "helpfulCount")); + default -> PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + }; } - reviewReplyRepository.findByReview_Id(reviewId).ifPresent(reviewReplyRepository::delete); - reviewPolicyRatingRepository.deleteAll(reviewPolicyRatingRepository.findByReview_Id(reviewId)); - reviewRepository.delete(r); - - Workplace wp = workplaceRepository.findById(workplaceId) - .orElseThrow(() -> new HandleException( - ErrorCode.WORKPLACE_NOT_FOUND, - "Workplace not found" - )); - long currentCount = wp.getReviewCount(); - wp.setReviewCount(Math.max(0, currentCount - 1)); - workplaceRepository.save(wp); - } - - // === HELPERS === - private Pageable makeSort(int page, int size, String sortBy) { - if (sortBy == null) { - return PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); + private ReviewResponse toResponse(Review r, boolean withExtras, User currentUser) { + boolean isHelpful = false; + if (currentUser != null) { + isHelpful = reviewReactionRepository.existsByReview_IdAndUser_Id(r.getId(), + currentUser.getId()); + } + return toResponse(r, withExtras, isHelpful); } - return switch (sortBy) { - case "ratingAsc" -> PageRequest.of(page, size, Sort.by(Sort.Direction.ASC, "overallRating")); - case "ratingDesc" -> PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "overallRating")); - case "helpfulness" -> PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "helpfulCount")); - default -> PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "createdAt")); - }; - } - - private ReviewResponse toResponse(Review r, boolean withExtras) { - Map policies = Collections.emptyMap(); - ReplyResponse replyDto = null; - if (withExtras) { - policies = reviewPolicyRatingRepository.findByReview_Id(r.getId()).stream() - .collect(Collectors.toMap( - rpr -> rpr.getPolicy().getLabel(), - ReviewPolicyRating::getScore - )); - replyDto = reviewReplyRepository.findByReview_Id(r.getId()) - .map(this::toReplyResponse) - .orElse(null); + + private ReviewResponse toResponse(Review r, boolean withExtras, boolean isHelpfulByUser) { + Map policies = Collections.emptyMap(); + ReplyResponse replyDto = null; + if (withExtras) { + policies = reviewPolicyRatingRepository.findByReview_Id(r.getId()).stream() + .collect(Collectors.toMap( + rpr -> rpr.getPolicy().getLabel(), + ReviewPolicyRating::getScore)); + replyDto = reviewReplyRepository.findByReview_Id(r.getId()) + .map(this::toReplyResponse) + .orElse(null); + } + + String nameSurname = profileRepository.findByUserId(r.getUser().getId()) + .map(p -> p.getFirstName() + " " + p.getLastName()) + .orElse(""); + + return ReviewResponse.builder() + .id(r.getId()) + .workplaceId(r.getWorkplace().getId()) + .userId(r.isAnonymous() ? null : r.getUser().getId()) + .username(r.isAnonymous() ? "" : r.getUser().getUsername()) + .nameSurname(r.isAnonymous() ? "anonymousUser" : nameSurname) + .title(r.getTitle()) + .content(r.getContent()) + .overallRating(r.getOverallRating()) + .anonymous(r.isAnonymous()) + .helpfulCount(r.getHelpfulCount()) + .isHelpfulByUser(isHelpfulByUser) + .ethicalPolicyRatings(policies) + .reply(replyDto) + .createdAt(r.getCreatedAt()) + .updatedAt(r.getUpdatedAt()) + .build(); } - String nameSurname = profileRepository.findByUserId(r.getUser().getId()) - .map(p -> p.getFirstName() + " " + p.getLastName()) - .orElse(""); - - return ReviewResponse.builder() - .id(r.getId()) - .workplaceId(r.getWorkplace().getId()) - .userId(r.isAnonymous() ? null : r.getUser().getId()) - .username(r.isAnonymous() ? "" : r.getUser().getUsername()) - .nameSurname(r.isAnonymous() ? "anonymousUser" : nameSurname) - .title(r.getTitle()) - .content(r.getContent()) - .overallRating(r.getOverallRating()) - .anonymous(r.isAnonymous()) - .helpfulCount(r.getHelpfulCount()) - .ethicalPolicyRatings(policies) - .reply(replyDto) - .createdAt(r.getCreatedAt()) - .updatedAt(r.getUpdatedAt()) - .build(); - } - - private ReplyResponse toReplyResponse(ReviewReply reply) { - return ReplyResponse.builder() - .id(reply.getId()) - .reviewId(reply.getReview().getId()) - .employerUserId(reply.getEmployerUser() != null ? reply.getEmployerUser().getId() : null) - .workplaceName(reply.getReview().getWorkplace().getCompanyName()) - .content(reply.getContent()) - .createdAt(reply.getCreatedAt()) - .updatedAt(reply.getUpdatedAt()) - .build(); - } + private ReplyResponse toReplyResponse(ReviewReply reply) { + return ReplyResponse.builder() + .id(reply.getId()) + .reviewId(reply.getReview().getId()) + .employerUserId(reply.getEmployerUser() != null ? reply.getEmployerUser().getId() + : null) + .workplaceName(reply.getReview().getWorkplace().getCompanyName()) + .content(reply.getContent()) + .createdAt(reply.getCreatedAt()) + .updatedAt(reply.getUpdatedAt()) + .build(); + } } \ No newline at end of file diff --git a/apps/jobboard-backend/src/test/java/org/bounswe/jobboardbackend/workplace/controller/WorkplaceReviewControllerTest.java b/apps/jobboard-backend/src/test/java/org/bounswe/jobboardbackend/workplace/controller/WorkplaceReviewControllerTest.java index 8a7dc4cf..2ac25a04 100644 --- a/apps/jobboard-backend/src/test/java/org/bounswe/jobboardbackend/workplace/controller/WorkplaceReviewControllerTest.java +++ b/apps/jobboard-backend/src/test/java/org/bounswe/jobboardbackend/workplace/controller/WorkplaceReviewControllerTest.java @@ -40,399 +40,445 @@ @WebMvcTest(controllers = WorkplaceReviewController.class) class WorkplaceReviewControllerTest { - @Autowired - private MockMvc mockMvc; - - @MockitoBean - private ReviewService reviewService; - - @MockitoBean - private UserRepository userRepository; - - @Autowired - private ObjectMapper objectMapper; - - private User userEntity(Long id) { - return User.builder() - .id(id) - .username("user" + id) - .email("user" + id + "@test.com") - .password("password-" + id) - .role(Role.ROLE_JOBSEEKER) - .emailVerified(true) - .build(); - } - - private final User USER_1 = userEntity(1L); - private final User USER_2 = userEntity(2L); - - // ======================================================================== - // CREATE REVIEW (POST /api/workplace/{workplaceId}/review) - // ======================================================================== - - @Test - void create_whenPrincipalIsDomainUser_callsServiceWithSameUser() throws Exception { - Long workplaceId = 10L; - - Authentication auth = new UsernamePasswordAuthenticationToken( - USER_1, - null, - Collections.emptyList() - ); - - ReviewCreateRequest req = new ReviewCreateRequest(); - String body = objectMapper.writeValueAsString(req); - - ReviewResponse response = new ReviewResponse(); - when(reviewService.createReview(eq(workplaceId), any(ReviewCreateRequest.class), eq(USER_1))) - .thenReturn(response); - - mockMvc.perform(post("/api/workplace/{workplaceId}/review", workplaceId) - .with(authentication(auth)) - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andExpect(status().isOk()); - - ArgumentCaptor workplaceCaptor = ArgumentCaptor.forClass(Long.class); - ArgumentCaptor reqCaptor = ArgumentCaptor.forClass(ReviewCreateRequest.class); - ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); - - verify(reviewService).createReview(workplaceCaptor.capture(), reqCaptor.capture(), userCaptor.capture()); - assertThat(workplaceCaptor.getValue()).isEqualTo(workplaceId); - assertThat(userCaptor.getValue().getId()).isEqualTo(USER_1.getId()); - } - - @Test - void create_whenUserDetailsPrincipal_resolvesUserFromEmail_andCallsService() throws Exception { - Long workplaceId = 11L; - User domainUser = USER_2; - String principalKey = domainUser.getEmail(); - - when(userRepository.findByEmail(principalKey)).thenReturn(Optional.of(domainUser)); - - ReviewCreateRequest req = new ReviewCreateRequest(); - String body = objectMapper.writeValueAsString(req); - - when(reviewService.createReview(eq(workplaceId), any(ReviewCreateRequest.class), eq(domainUser))) - .thenReturn(new ReviewResponse()); - - mockMvc.perform(post("/api/workplace/{workplaceId}/review", workplaceId) - .with(user(principalKey)) - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andExpect(status().isOk()); - - ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); - verify(reviewService).createReview(eq(workplaceId), any(ReviewCreateRequest.class), userCaptor.capture()); - assertThat(userCaptor.getValue().getId()).isEqualTo(domainUser.getId()); - } - - @Test - void create_whenUserDetailsPrincipalUserNotFound_returnsForbidden_andDoesNotCallService() throws Exception { - Long workplaceId = 12L; - String principalKey = "unknown@test.com"; - - when(userRepository.findByEmail(principalKey)).thenReturn(Optional.empty()); - when(userRepository.findByUsername(principalKey)).thenReturn(Optional.empty()); - - ReviewCreateRequest req = new ReviewCreateRequest(); - String body = objectMapper.writeValueAsString(req); - - mockMvc.perform(post("/api/workplace/{workplaceId}/review", workplaceId) - .with(user(principalKey)) - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andExpect(status().isForbidden()); - - verify(reviewService, never()).createReview(anyLong(), any(), any()); - } - - @Test - void create_whenPrincipalIsName_resolvesUserFromName_andCallsService() throws Exception { - Long workplaceId = 13L; - User domainUser = USER_1; - String name = domainUser.getEmail(); - - when(userRepository.findByEmail(name)).thenReturn(Optional.of(domainUser)); - - Authentication auth = new UsernamePasswordAuthenticationToken( - name, - "pwd", - Collections.emptyList() - ); - - ReviewCreateRequest req = new ReviewCreateRequest(); - String body = objectMapper.writeValueAsString(req); - - when(reviewService.createReview(eq(workplaceId), any(ReviewCreateRequest.class), eq(domainUser))) - .thenReturn(new ReviewResponse()); - - mockMvc.perform(post("/api/workplace/{workplaceId}/review", workplaceId) - .with(authentication(auth)) - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andExpect(status().isOk()); - - ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); - verify(reviewService).createReview(eq(workplaceId), any(ReviewCreateRequest.class), userCaptor.capture()); - assertThat(userCaptor.getValue().getId()).isEqualTo(domainUser.getId()); - } - - @Test - void create_whenUnauthenticated_returnsUnauthorized_andDoesNotCallService() throws Exception { - Long workplaceId = 14L; - - ReviewCreateRequest req = new ReviewCreateRequest(); - String body = objectMapper.writeValueAsString(req); - - mockMvc.perform(post("/api/workplace/{workplaceId}/review", workplaceId) - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andExpect(status().isUnauthorized()); - - verify(reviewService, never()).createReview(anyLong(), any(), any()); - } - - // ======================================================================== - // LIST REVIEWS (GET /api/workplace/{workplaceId}/review) - // ======================================================================== - - @Test - void list_whenCalledWithAllParams_delegatesToService() throws Exception { - Long workplaceId = 20L; - int page = 1; - int size = 5; - String ratingFilter = "3.5,4.2"; - String sortBy = "rating"; - Boolean hasComment = true; - String policy = "ENVIRONMENT"; - Integer policyMin = 2; - - PaginatedResponse response = - PaginatedResponse.of(Collections.emptyList(), page, size, 0); - when(reviewService.listReviews( - anyLong(), anyInt(), anyInt(), any(), any(), any(), any(), any()) - ).thenReturn(response); - - mockMvc.perform(get("/api/workplace/{workplaceId}/review", workplaceId) - .with(user("user1@test.com")) - .param("page", String.valueOf(page)) - .param("size", String.valueOf(size)) - .param("ratingFilter", ratingFilter) - .param("sortBy", sortBy) - .param("hasComment", String.valueOf(hasComment)) - .param("policy", policy) - .param("policyMin", String.valueOf(policyMin))) - .andExpect(status().isOk()); - - ArgumentCaptor wpCaptor = ArgumentCaptor.forClass(Long.class); - ArgumentCaptor pageCaptor = ArgumentCaptor.forClass(Integer.class); - ArgumentCaptor sizeCaptor = ArgumentCaptor.forClass(Integer.class); - ArgumentCaptor ratingCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor sortCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor hasCommentCaptor = ArgumentCaptor.forClass(Boolean.class); - ArgumentCaptor policyCaptor = ArgumentCaptor.forClass(String.class); - ArgumentCaptor policyMinCaptor = ArgumentCaptor.forClass(Integer.class); - - verify(reviewService).listReviews( - wpCaptor.capture(), - pageCaptor.capture(), - sizeCaptor.capture(), - ratingCaptor.capture(), - sortCaptor.capture(), - hasCommentCaptor.capture(), - policyCaptor.capture(), - policyMinCaptor.capture() - ); - - assertThat(wpCaptor.getValue()).isEqualTo(workplaceId); - assertThat(pageCaptor.getValue()).isEqualTo(page); - assertThat(sizeCaptor.getValue()).isEqualTo(size); - assertThat(ratingCaptor.getValue()).isEqualTo(ratingFilter); - assertThat(sortCaptor.getValue()).isEqualTo(sortBy); - assertThat(hasCommentCaptor.getValue()).isEqualTo(hasComment); - assertThat(policyCaptor.getValue()).isEqualTo(policy); - assertThat(policyMinCaptor.getValue()).isEqualTo(policyMin); - } - - @Test - void list_whenCalledWithoutParams_usesDefaults() throws Exception { - Long workplaceId = 21L; - - PaginatedResponse response = - PaginatedResponse.of(Collections.emptyList(), 0, 10, 0); - when(reviewService.listReviews( - anyLong(), anyInt(), anyInt(), any(), any(), any(), any(), any()) - ).thenReturn(response); - - mockMvc.perform(get("/api/workplace/{workplaceId}/review", workplaceId) - .with(user("user1@test.com"))) - .andExpect(status().isOk()); - - ArgumentCaptor pageCaptor = ArgumentCaptor.forClass(Integer.class); - ArgumentCaptor sizeCaptor = ArgumentCaptor.forClass(Integer.class); - - verify(reviewService).listReviews( - anyLong(), - pageCaptor.capture(), - sizeCaptor.capture(), - any(), - any(), - any(), - any(), - any() - ); - - assertThat(pageCaptor.getValue()).isEqualTo(0); - assertThat(sizeCaptor.getValue()).isEqualTo(10); - } - - // ======================================================================== - // GET ONE REVIEW (GET /api/workplace/{workplaceId}/review/{reviewId}) - // ======================================================================== - - @Test - void getOne_whenCalled_delegatesToService() throws Exception { - Long workplaceId = 30L; - Long reviewId = 300L; - - ReviewResponse res = new ReviewResponse(); - when(reviewService.getOne(workplaceId, reviewId)).thenReturn(res); - - mockMvc.perform(get("/api/workplace/{workplaceId}/review/{reviewId}", workplaceId, reviewId) - .with(user("user1@test.com"))) - .andExpect(status().isOk()); - - verify(reviewService).getOne(workplaceId, reviewId); - } - - // ======================================================================== - // UPDATE REVIEW (PUT /api/workplace/{workplaceId}/review/{reviewId}) - // ======================================================================== - - @Test - void update_whenPrincipalIsDomainUser_callsServiceWithSameUser() throws Exception { - Long workplaceId = 40L; - Long reviewId = 400L; - - Authentication auth = new UsernamePasswordAuthenticationToken( - USER_1, - null, - Collections.emptyList() - ); - - ReviewUpdateRequest req = new ReviewUpdateRequest(); - String body = objectMapper.writeValueAsString(req); - - ReviewResponse response = new ReviewResponse(); - when(reviewService.updateReview(eq(workplaceId), eq(reviewId), any(ReviewUpdateRequest.class), eq(USER_1))) - .thenReturn(response); - - mockMvc.perform(put("/api/workplace/{workplaceId}/review/{reviewId}", workplaceId, reviewId) - .with(authentication(auth)) - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andExpect(status().isOk()); - - verify(reviewService).updateReview(eq(workplaceId), eq(reviewId), any(ReviewUpdateRequest.class), eq(USER_1)); - } - - @Test - void update_whenUserDetailsPrincipalUserNotFound_returnsForbidden_andDoesNotCallService() throws Exception { - Long workplaceId = 41L; - Long reviewId = 401L; - String principalKey = "unknown@test.com"; - - when(userRepository.findByEmail(principalKey)).thenReturn(Optional.empty()); - when(userRepository.findByUsername(principalKey)).thenReturn(Optional.empty()); - - ReviewUpdateRequest req = new ReviewUpdateRequest(); - String body = objectMapper.writeValueAsString(req); - - mockMvc.perform(put("/api/workplace/{workplaceId}/review/{reviewId}", workplaceId, reviewId) - .with(user(principalKey)) - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andExpect(status().isForbidden()); - - verify(reviewService, never()).updateReview(anyLong(), anyLong(), any(), any()); - } - - @Test - void update_whenUnauthenticated_returnsUnauthorized_andDoesNotCallService() throws Exception { - Long workplaceId = 42L; - Long reviewId = 402L; - - ReviewUpdateRequest req = new ReviewUpdateRequest(); - String body = objectMapper.writeValueAsString(req); - - mockMvc.perform(put("/api/workplace/{workplaceId}/review/{reviewId}", workplaceId, reviewId) - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(body)) - .andExpect(status().isUnauthorized()); - - verify(reviewService, never()).updateReview(anyLong(), anyLong(), any(), any()); - } - - // ======================================================================== - // DELETE REVIEW (DELETE /api/workplace/{workplaceId}/review/{reviewId}) - // ======================================================================== - - @Test - void delete_whenPrincipalIsDomainUser_callsServiceWithIsAdminFalse_andReturnsApiMessage() throws Exception { - Long workplaceId = 50L; - Long reviewId = 500L; - - Authentication auth = new UsernamePasswordAuthenticationToken( - USER_1, - null, - Collections.emptyList() - ); - - mockMvc.perform(delete("/api/workplace/{workplaceId}/review/{reviewId}", workplaceId, reviewId) - .with(authentication(auth)) - .with(csrf())) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Review deleted")) - .andExpect(jsonPath("$.code").value("REVIEW_DELETED")); - - verify(reviewService).deleteReview(workplaceId, reviewId, USER_1, false); - } - - @Test - void delete_whenUserDetailsPrincipalUserNotFound_returnsForbidden_andDoesNotCallService() throws Exception { - Long workplaceId = 51L; - Long reviewId = 501L; - String principalKey = "unknown@test.com"; - - when(userRepository.findByEmail(principalKey)).thenReturn(Optional.empty()); - when(userRepository.findByUsername(principalKey)).thenReturn(Optional.empty()); - - mockMvc.perform(delete("/api/workplace/{workplaceId}/review/{reviewId}", workplaceId, reviewId) - .with(user(principalKey)) - .with(csrf())) - .andExpect(status().isForbidden()); - - verify(reviewService, never()).deleteReview(anyLong(), anyLong(), any(), anyBoolean()); - } - - @Test - void delete_whenUnauthenticated_returnsUnauthorized_andDoesNotCallService() throws Exception { - Long workplaceId = 52L; - Long reviewId = 502L; - - mockMvc.perform(delete("/api/workplace/{workplaceId}/review/{reviewId}", workplaceId, reviewId) - .with(csrf())) - .andExpect(status().isUnauthorized()); - - verify(reviewService, never()).deleteReview(anyLong(), anyLong(), any(), anyBoolean()); - } + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private ReviewService reviewService; + + @MockitoBean + private UserRepository userRepository; + + @Autowired + private ObjectMapper objectMapper; + + private User userEntity(Long id) { + return User.builder() + .id(id) + .username("user" + id) + .email("user" + id + "@test.com") + .password("password-" + id) + .role(Role.ROLE_JOBSEEKER) + .emailVerified(true) + .build(); + } + + private final User USER_1 = userEntity(1L); + private final User USER_2 = userEntity(2L); + + // ======================================================================== + // CREATE REVIEW (POST /api/workplace/{workplaceId}/review) + // ======================================================================== + + @Test + void create_whenPrincipalIsDomainUser_callsServiceWithSameUser() throws Exception { + Long workplaceId = 10L; + + Authentication auth = new UsernamePasswordAuthenticationToken( + USER_1, + null, + Collections.emptyList()); + + ReviewCreateRequest req = new ReviewCreateRequest(); + String body = objectMapper.writeValueAsString(req); + + ReviewResponse response = new ReviewResponse(); + when(reviewService.createReview(eq(workplaceId), any(ReviewCreateRequest.class), eq(USER_1))) + .thenReturn(response); + + mockMvc.perform(post("/api/workplace/{workplaceId}/review", workplaceId) + .with(authentication(auth)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isOk()); + + ArgumentCaptor workplaceCaptor = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor reqCaptor = ArgumentCaptor.forClass(ReviewCreateRequest.class); + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + + verify(reviewService).createReview(workplaceCaptor.capture(), reqCaptor.capture(), + userCaptor.capture()); + assertThat(workplaceCaptor.getValue()).isEqualTo(workplaceId); + assertThat(userCaptor.getValue().getId()).isEqualTo(USER_1.getId()); + } + + @Test + void create_whenUserDetailsPrincipal_resolvesUserFromEmail_andCallsService() throws Exception { + Long workplaceId = 11L; + User domainUser = USER_2; + String principalKey = domainUser.getEmail(); + + when(userRepository.findByEmail(principalKey)).thenReturn(Optional.of(domainUser)); + + ReviewCreateRequest req = new ReviewCreateRequest(); + String body = objectMapper.writeValueAsString(req); + + when(reviewService.createReview(eq(workplaceId), any(ReviewCreateRequest.class), eq(domainUser))) + .thenReturn(new ReviewResponse()); + + mockMvc.perform(post("/api/workplace/{workplaceId}/review", workplaceId) + .with(user(principalKey)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isOk()); + + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + verify(reviewService).createReview(eq(workplaceId), any(ReviewCreateRequest.class), + userCaptor.capture()); + assertThat(userCaptor.getValue().getId()).isEqualTo(domainUser.getId()); + } + + @Test + void create_whenUserDetailsPrincipalUserNotFound_returnsForbidden_andDoesNotCallService() throws Exception { + Long workplaceId = 12L; + String principalKey = "unknown@test.com"; + + when(userRepository.findByEmail(principalKey)).thenReturn(Optional.empty()); + when(userRepository.findByUsername(principalKey)).thenReturn(Optional.empty()); + + ReviewCreateRequest req = new ReviewCreateRequest(); + String body = objectMapper.writeValueAsString(req); + + mockMvc.perform(post("/api/workplace/{workplaceId}/review", workplaceId) + .with(user(principalKey)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isForbidden()); + + verify(reviewService, never()).createReview(anyLong(), any(), any()); + } + + @Test + void create_whenPrincipalIsName_resolvesUserFromName_andCallsService() throws Exception { + Long workplaceId = 13L; + User domainUser = USER_1; + String name = domainUser.getEmail(); + + when(userRepository.findByEmail(name)).thenReturn(Optional.of(domainUser)); + + Authentication auth = new UsernamePasswordAuthenticationToken( + name, + "pwd", + Collections.emptyList()); + + ReviewCreateRequest req = new ReviewCreateRequest(); + String body = objectMapper.writeValueAsString(req); + + when(reviewService.createReview(eq(workplaceId), any(ReviewCreateRequest.class), eq(domainUser))) + .thenReturn(new ReviewResponse()); + + mockMvc.perform(post("/api/workplace/{workplaceId}/review", workplaceId) + .with(authentication(auth)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isOk()); + + ArgumentCaptor userCaptor = ArgumentCaptor.forClass(User.class); + verify(reviewService).createReview(eq(workplaceId), any(ReviewCreateRequest.class), + userCaptor.capture()); + assertThat(userCaptor.getValue().getId()).isEqualTo(domainUser.getId()); + } + + @Test + void create_whenUnauthenticated_returnsUnauthorized_andDoesNotCallService() throws Exception { + Long workplaceId = 14L; + + ReviewCreateRequest req = new ReviewCreateRequest(); + String body = objectMapper.writeValueAsString(req); + + mockMvc.perform(post("/api/workplace/{workplaceId}/review", workplaceId) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isUnauthorized()); + + verify(reviewService, never()).createReview(anyLong(), any(), any()); + } + + // ======================================================================== + // LIST REVIEWS (GET /api/workplace/{workplaceId}/review) + // ======================================================================== + + @Test + void list_whenCalledWithAllParams_delegatesToService() throws Exception { + Long workplaceId = 20L; + int page = 1; + int size = 5; + String ratingFilter = "3.5,4.2"; + String sortBy = "rating"; + Boolean hasComment = true; + String policy = "ENVIRONMENT"; + Integer policyMin = 2; + + PaginatedResponse response = PaginatedResponse.of(Collections.emptyList(), page, size, + 0); + when(reviewService.listReviews( + anyLong(), anyInt(), anyInt(), any(), any(), any(), any(), any(), any())) + .thenReturn(response); + + Authentication auth = new UsernamePasswordAuthenticationToken( + USER_1, + null, + Collections.emptyList()); + + mockMvc.perform(get("/api/workplace/{workplaceId}/review", workplaceId) + .with(authentication(auth)) + .param("page", String.valueOf(page)) + .param("size", String.valueOf(size)) + .param("ratingFilter", ratingFilter) + .param("sortBy", sortBy) + .param("hasComment", String.valueOf(hasComment)) + .param("policy", policy) + .param("policyMin", String.valueOf(policyMin))) + .andExpect(status().isOk()); + + ArgumentCaptor wpCaptor = ArgumentCaptor.forClass(Long.class); + ArgumentCaptor pageCaptor = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor sizeCaptor = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor ratingCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor sortCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor hasCommentCaptor = ArgumentCaptor.forClass(Boolean.class); + ArgumentCaptor policyCaptor = ArgumentCaptor.forClass(String.class); + ArgumentCaptor policyMinCaptor = ArgumentCaptor.forClass(Integer.class); + + verify(reviewService).listReviews( + wpCaptor.capture(), + pageCaptor.capture(), + sizeCaptor.capture(), + ratingCaptor.capture(), + sortCaptor.capture(), + hasCommentCaptor.capture(), + policyCaptor.capture(), + policyMinCaptor.capture(), + any()); + + assertThat(wpCaptor.getValue()).isEqualTo(workplaceId); + assertThat(pageCaptor.getValue()).isEqualTo(page); + assertThat(sizeCaptor.getValue()).isEqualTo(size); + assertThat(ratingCaptor.getValue()).isEqualTo(ratingFilter); + assertThat(sortCaptor.getValue()).isEqualTo(sortBy); + assertThat(hasCommentCaptor.getValue()).isEqualTo(hasComment); + assertThat(policyCaptor.getValue()).isEqualTo(policy); + assertThat(policyMinCaptor.getValue()).isEqualTo(policyMin); + } + + @Test + void list_whenCalledWithoutParams_usesDefaults() throws Exception { + Long workplaceId = 21L; + + PaginatedResponse response = PaginatedResponse.of(Collections.emptyList(), 0, 10, 0); + when(reviewService.listReviews( + anyLong(), anyInt(), anyInt(), any(), any(), any(), any(), any(), any())) + .thenReturn(response); + + Authentication auth = new UsernamePasswordAuthenticationToken( + USER_1, + null, + Collections.emptyList()); + + mockMvc.perform(get("/api/workplace/{workplaceId}/review", workplaceId) + .with(authentication(auth))) + .andExpect(status().isOk()); + + ArgumentCaptor pageCaptor = ArgumentCaptor.forClass(Integer.class); + ArgumentCaptor sizeCaptor = ArgumentCaptor.forClass(Integer.class); + + verify(reviewService).listReviews( + anyLong(), + pageCaptor.capture(), + sizeCaptor.capture(), + any(), + any(), + any(), + any(), + any(), + any()); + + assertThat(pageCaptor.getValue()).isEqualTo(0); + assertThat(sizeCaptor.getValue()).isEqualTo(10); + } + + // ======================================================================== + // GET ONE REVIEW (GET /api/workplace/{workplaceId}/review/{reviewId}) + // ======================================================================== + + @Test + void getOne_whenCalled_delegatesToService() throws Exception { + Long workplaceId = 30L; + Long reviewId = 300L; + + ReviewResponse res = new ReviewResponse(); + when(reviewService.getOne(eq(workplaceId), eq(reviewId), any())).thenReturn(res); + + Authentication auth = new UsernamePasswordAuthenticationToken( + USER_1, + null, + Collections.emptyList()); + + mockMvc.perform(get("/api/workplace/{workplaceId}/review/{reviewId}", workplaceId, reviewId) + .with(authentication(auth))) + .andExpect(status().isOk()); + + verify(reviewService).getOne(eq(workplaceId), eq(reviewId), any()); + } + + // ======================================================================== + // UPDATE REVIEW (PUT /api/workplace/{workplaceId}/review/{reviewId}) + // ======================================================================== + + @Test + void update_whenPrincipalIsDomainUser_callsServiceWithSameUser() throws Exception { + Long workplaceId = 40L; + Long reviewId = 400L; + + Authentication auth = new UsernamePasswordAuthenticationToken( + USER_1, + null, + Collections.emptyList()); + + ReviewUpdateRequest req = new ReviewUpdateRequest(); + String body = objectMapper.writeValueAsString(req); + + ReviewResponse response = new ReviewResponse(); + when(reviewService.updateReview(eq(workplaceId), eq(reviewId), any(ReviewUpdateRequest.class), + eq(USER_1))) + .thenReturn(response); + + mockMvc.perform(put("/api/workplace/{workplaceId}/review/{reviewId}", workplaceId, reviewId) + .with(authentication(auth)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isOk()); + + verify(reviewService).updateReview(eq(workplaceId), eq(reviewId), any(ReviewUpdateRequest.class), + eq(USER_1)); + } + + @Test + void update_whenUserDetailsPrincipalUserNotFound_returnsForbidden_andDoesNotCallService() throws Exception { + Long workplaceId = 41L; + Long reviewId = 401L; + String principalKey = "unknown@test.com"; + + when(userRepository.findByEmail(principalKey)).thenReturn(Optional.empty()); + when(userRepository.findByUsername(principalKey)).thenReturn(Optional.empty()); + + ReviewUpdateRequest req = new ReviewUpdateRequest(); + String body = objectMapper.writeValueAsString(req); + + mockMvc.perform(put("/api/workplace/{workplaceId}/review/{reviewId}", workplaceId, reviewId) + .with(user(principalKey)) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isForbidden()); + + verify(reviewService, never()).updateReview(anyLong(), anyLong(), any(), any()); + } + + @Test + void update_whenUnauthenticated_returnsUnauthorized_andDoesNotCallService() throws Exception { + Long workplaceId = 42L; + Long reviewId = 402L; + + ReviewUpdateRequest req = new ReviewUpdateRequest(); + String body = objectMapper.writeValueAsString(req); + + mockMvc.perform(put("/api/workplace/{workplaceId}/review/{reviewId}", workplaceId, reviewId) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(body)) + .andExpect(status().isUnauthorized()); + + verify(reviewService, never()).updateReview(anyLong(), anyLong(), any(), any()); + } + + // ======================================================================== + // DELETE REVIEW (DELETE /api/workplace/{workplaceId}/review/{reviewId}) + // ======================================================================== + + @Test + void delete_whenPrincipalIsDomainUser_callsServiceWithIsAdminFalse_andReturnsApiMessage() throws Exception { + Long workplaceId = 50L; + Long reviewId = 500L; + + Authentication auth = new UsernamePasswordAuthenticationToken( + USER_1, + null, + Collections.emptyList()); + + mockMvc.perform(delete("/api/workplace/{workplaceId}/review/{reviewId}", workplaceId, reviewId) + .with(authentication(auth)) + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Review deleted")) + .andExpect(jsonPath("$.code").value("REVIEW_DELETED")); + + verify(reviewService).deleteReview(workplaceId, reviewId, USER_1, false); + } + + @Test + void delete_whenUserDetailsPrincipalUserNotFound_returnsForbidden_andDoesNotCallService() throws Exception { + Long workplaceId = 51L; + Long reviewId = 501L; + String principalKey = "unknown@test.com"; + + when(userRepository.findByEmail(principalKey)).thenReturn(Optional.empty()); + when(userRepository.findByUsername(principalKey)).thenReturn(Optional.empty()); + + mockMvc.perform(delete("/api/workplace/{workplaceId}/review/{reviewId}", workplaceId, reviewId) + .with(user(principalKey)) + .with(csrf())) + .andExpect(status().isForbidden()); + + verify(reviewService, never()).deleteReview(anyLong(), anyLong(), any(), anyBoolean()); + } + + @Test + void delete_whenUnauthenticated_returnsUnauthorized_andDoesNotCallService() throws Exception { + Long workplaceId = 52L; + Long reviewId = 502L; + + mockMvc.perform(delete("/api/workplace/{workplaceId}/review/{reviewId}", workplaceId, reviewId) + .with(csrf())) + .andExpect(status().isUnauthorized()); + + verify(reviewService, never()).deleteReview(anyLong(), anyLong(), any(), anyBoolean()); + } + + // ======================================================================== + // TOGGLE HELPFUL (POST /api/workplace/{workplaceId}/review/{reviewId}/helpful) + // ======================================================================== + + @Test + void toggleHelpful_whenAuthenticated_callsService() throws Exception { + Long workplaceId = 60L; + Long reviewId = 600L; + + Authentication auth = new UsernamePasswordAuthenticationToken( + USER_1, + null, + Collections.emptyList()); + + ReviewResponse response = new ReviewResponse(); + response.setHelpfulCount(1); + response.setHelpfulByUser(true); + + when(reviewService.toggleHelpful(eq(workplaceId), eq(reviewId), eq(USER_1))) + .thenReturn(response); + + mockMvc.perform(post("/api/workplace/{workplaceId}/review/{reviewId}/helpful", workplaceId, reviewId) + .with(authentication(auth)) + .with(csrf())) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.helpfulCount").value(1)) + .andExpect(jsonPath("$.helpfulByUser").value(true)); + + verify(reviewService).toggleHelpful(workplaceId, reviewId, USER_1); + } } diff --git a/apps/jobboard-backend/src/test/java/org/bounswe/jobboardbackend/workplace/service/ReviewServiceTest.java b/apps/jobboard-backend/src/test/java/org/bounswe/jobboardbackend/workplace/service/ReviewServiceTest.java index 5acbbc3e..ce60f1b8 100644 --- a/apps/jobboard-backend/src/test/java/org/bounswe/jobboardbackend/workplace/service/ReviewServiceTest.java +++ b/apps/jobboard-backend/src/test/java/org/bounswe/jobboardbackend/workplace/service/ReviewServiceTest.java @@ -32,863 +32,926 @@ @ExtendWith(MockitoExtension.class) class ReviewServiceTest { - @Mock - private WorkplaceRepository workplaceRepository; - - @Mock - private ReviewRepository reviewRepository; - - @Mock - private ReviewPolicyRatingRepository reviewPolicyRatingRepository; - - @Mock - private ReviewReplyRepository reviewReplyRepository; - - @Mock - private EmployerWorkplaceRepository employerWorkplaceRepository; - - @Mock - private UserRepository userRepository; - - @Mock - private ProfileRepository profileRepository; - - @InjectMocks - private ReviewService reviewService; - - // --------- helpers --------- - - private Workplace sampleWorkplace(Long id) { - Workplace wp = new Workplace(); - wp.setId(id); - wp.setCompanyName("Acme Inc"); - wp.setEthicalTags(Set.of(EthicalPolicy.SALARY_TRANSPARENCY)); - wp.setReviewCount(0L); - return wp; - } - - private User sampleUser(Long id) { - User u = new User(); - u.setId(id); - u.setUsername("user" + id); - u.setEmail("user" + id + "@test.com"); - return u; - } - - private Profile sampleProfile(Long userId) { - Profile p = new Profile(); - p.setId(userId); - p.setFirstName("John"); - p.setLastName("Doe"); - return p; - } - - private Review sampleReview(Long id, Workplace wp, User user) { - Review r = Review.builder() - .id(id) - .workplace(wp) - .user(user) - .title("Good place") - .content("Nice culture") - .overallRating(4.0) - .anonymous(false) - .helpfulCount(0) - .build(); - r.setCreatedAt(Instant.now()); - r.setUpdatedAt(Instant.now()); - return r; - } - - // ========== CREATE REVIEW ========== - - @Test - void createReview_whenValidRequest_createsReviewAndReturnsResponse() { - Long workplaceId = 1L; - Long userId = 10L; - - Workplace wp = sampleWorkplace(workplaceId); - User user = sampleUser(userId); - Profile profile = sampleProfile(userId); - - ReviewCreateRequest req = new ReviewCreateRequest(); - req.setTitle("Great workplace"); - req.setContent("Very ethical"); - Map policyRatings = new HashMap<>(); - policyRatings.put(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 4); - req.setEthicalPolicyRatings(policyRatings); - req.setAnonymous(false); - - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); - when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(false); - when(reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(false); - when(userRepository.findById(userId)).thenReturn(Optional.of(user)); - - when(reviewRepository.save(any(Review.class))).thenAnswer(invocation -> { - Review r = invocation.getArgument(0); - if (r.getId() == null) { - r.setId(100L); - } - if (r.getCreatedAt() == null) r.setCreatedAt(Instant.now()); - if (r.getUpdatedAt() == null) r.setUpdatedAt(Instant.now()); - return r; - }); - - when(reviewPolicyRatingRepository.findByReview_Id(anyLong())) - .thenReturn(List.of( - ReviewPolicyRating.builder() + @Mock + private WorkplaceRepository workplaceRepository; + + @Mock + private ReviewRepository reviewRepository; + + @Mock + private ReviewPolicyRatingRepository reviewPolicyRatingRepository; + + @Mock + private ReviewReplyRepository reviewReplyRepository; + + @Mock + private EmployerWorkplaceRepository employerWorkplaceRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private ProfileRepository profileRepository; + + @Mock + private ReviewReactionRepository reviewReactionRepository; + + @InjectMocks + private ReviewService reviewService; + + // --------- helpers --------- + + private Workplace sampleWorkplace(Long id) { + Workplace wp = new Workplace(); + wp.setId(id); + wp.setCompanyName("Acme Inc"); + wp.setEthicalTags(Set.of(EthicalPolicy.SALARY_TRANSPARENCY)); + wp.setReviewCount(0L); + return wp; + } + + private User sampleUser(Long id) { + User u = new User(); + u.setId(id); + u.setUsername("user" + id); + u.setEmail("user" + id + "@test.com"); + return u; + } + + private Profile sampleProfile(Long userId) { + Profile p = new Profile(); + p.setId(userId); + p.setFirstName("John"); + p.setLastName("Doe"); + return p; + } + + private Review sampleReview(Long id, Workplace wp, User user) { + Review r = Review.builder() + .id(id) + .workplace(wp) + .user(user) + .title("Good place") + .content("Nice culture") + .overallRating(4.0) + .anonymous(false) + .helpfulCount(0) + .build(); + r.setCreatedAt(Instant.now()); + r.setUpdatedAt(Instant.now()); + return r; + } + + // ========== CREATE REVIEW ========== + + @Test + void createReview_whenValidRequest_createsReviewAndReturnsResponse() { + Long workplaceId = 1L; + Long userId = 10L; + + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(userId); + Profile profile = sampleProfile(userId); + + ReviewCreateRequest req = new ReviewCreateRequest(); + req.setTitle("Great workplace"); + req.setContent("Very ethical"); + Map policyRatings = new HashMap<>(); + policyRatings.put(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 4); + req.setEthicalPolicyRatings(policyRatings); + req.setAnonymous(false); + + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); + when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(false); + when(reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(false); + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + + when(reviewRepository.save(any(Review.class))).thenAnswer(invocation -> { + Review r = invocation.getArgument(0); + if (r.getId() == null) { + r.setId(100L); + } + if (r.getCreatedAt() == null) + r.setCreatedAt(Instant.now()); + if (r.getUpdatedAt() == null) + r.setUpdatedAt(Instant.now()); + return r; + }); + + when(reviewPolicyRatingRepository.findByReview_Id(anyLong())) + .thenReturn(List.of( + ReviewPolicyRating.builder() + .id(1L) + .review(null) + .policy(EthicalPolicy.SALARY_TRANSPARENCY) + .score(4) + .build())); + when(reviewReplyRepository.findByReview_Id(anyLong())) + .thenReturn(Optional.empty()); + when(profileRepository.findByUserId(userId)) + .thenReturn(Optional.of(profile)); + + ReviewResponse res = reviewService.createReview(workplaceId, req, user); + + assertThat(res).isNotNull(); + assertThat(res.getWorkplaceId()).isEqualTo(workplaceId); + assertThat(res.getTitle()).isEqualTo("Great workplace"); + assertThat(res.getOverallRating()).isEqualTo(4.0); + assertThat(res.getEthicalPolicyRatings()) + .containsEntry(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 4); + + verify(workplaceRepository).save(wp); + verify(reviewPolicyRatingRepository, times(1)).findByReview_Id(anyLong()); + } + + @Test + void createReview_whenEmployerOfWorkplace_throwsHandleException() { + Long workplaceId = 1L; + Long userId = 10L; + + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(userId); + + ReviewCreateRequest req = new ReviewCreateRequest(); + req.setEthicalPolicyRatings( + Map.of(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 4)); + req.setTitle("x"); + req.setContent("y"); + + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); + when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(true); + + assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.WORKPLACE_UNAUTHORIZED); + } + + @Test + void createReview_whenAlreadyReviewed_throwsHandleException() { + Long workplaceId = 1L; + Long userId = 10L; + + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(userId); + + ReviewCreateRequest req = new ReviewCreateRequest(); + req.setEthicalPolicyRatings( + Map.of(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 4)); + req.setTitle("x"); + req.setContent("y"); + + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); + when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(false); + when(reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(true); + + assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.REVIEW_ALREADY_EXISTS); + } + + @Test + void createReview_whenWorkplaceNotFound_throwsHandleException() { + Long workplaceId = 1L; + User user = sampleUser(10L); + ReviewCreateRequest req = new ReviewCreateRequest(); + + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.WORKPLACE_NOT_FOUND); + } + + @Test + void createReview_whenWorkplaceHasNoEthicalTags_throwsHandleException() { + Long workplaceId = 1L; + Long userId = 10L; + Workplace wp = sampleWorkplace(workplaceId); + wp.setEthicalTags(Collections.emptySet()); + User user = sampleUser(userId); + + ReviewCreateRequest req = new ReviewCreateRequest(); + req.setEthicalPolicyRatings( + Map.of(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 4)); + + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); + when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(false); + when(reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(false); + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + + assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.VALIDATION_ERROR); + } + + @Test + void createReview_whenPolicyRatingsNull_throwsHandleException() { + Long workplaceId = 1L; + Long userId = 10L; + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(userId); + + ReviewCreateRequest req = new ReviewCreateRequest(); + req.setEthicalPolicyRatings(null); + + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); + when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(false); + when(reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(false); + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + + assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.VALIDATION_ERROR); + } + + @Test + void createReview_whenScoreOutOfRange_throwsHandleException() { + Long workplaceId = 1L; + Long userId = 10L; + + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(userId); + + ReviewCreateRequest req = new ReviewCreateRequest(); + req.setTitle("Bad score"); + req.setContent("Test"); + req.setEthicalPolicyRatings( + Map.of(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 6)); + + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); + when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(false); + when(reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(false); + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + + assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.VALIDATION_ERROR); + } + + @Test + void createReview_whenUnknownPolicyLabel_throwsHandleException() { + Long workplaceId = 1L; + Long userId = 10L; + + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(userId); + + ReviewCreateRequest req = new ReviewCreateRequest(); + req.setTitle("Unknown policy"); + req.setContent("Test"); + req.setEthicalPolicyRatings( + Map.of("UnknownPolicy", 4)); + + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); + when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(false); + when(reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(false); + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + + assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.VALIDATION_ERROR); + } + + @Test + void createReview_whenPolicyNotDeclaredByWorkplace_throwsHandleException() { + Long workplaceId = 1L; + Long userId = 10L; + + Workplace wp = sampleWorkplace(workplaceId); + wp.setEthicalTags(Set.of(EthicalPolicy.EQUAL_PAY_POLICY)); + User user = sampleUser(userId); + + ReviewCreateRequest req = new ReviewCreateRequest(); + req.setTitle("Undeclared policy"); + req.setContent("Test"); + req.setEthicalPolicyRatings( + Map.of(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 4)); + + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); + when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(false); + when(reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) + .thenReturn(false); + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + + assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.VALIDATION_ERROR); + } + + // ========== LIST REVIEWS ========== + + @Test + void listReviews_whenNoFilters_returnsPaginatedReviews() { + Long workplaceId = 1L; + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(10L); + Profile profile = sampleProfile(user.getId()); + + Review review = sampleReview(100L, wp, user); + Page page = new PageImpl<>(List.of(review), PageRequest.of(0, 10), 1); + + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); + when(reviewRepository.findByWorkplace_Id(eq(workplaceId), any(Pageable.class))) + .thenReturn(page); + + when(reviewPolicyRatingRepository.findByReview_Id(100L)) + .thenReturn(List.of( + ReviewPolicyRating.builder() + .id(1L) + .review(review) + .policy(EthicalPolicy.SALARY_TRANSPARENCY) + .score(4) + .build())); + when(reviewReplyRepository.findByReview_Id(100L)) + .thenReturn(Optional.empty()); + when(profileRepository.findByUserId(user.getId())) + .thenReturn(Optional.of(profile)); + + PaginatedResponse res = reviewService.listReviews( + workplaceId, 0, 10, + null, null, null, + null, null, null); + + assertThat(res).isNotNull(); + assertThat(res.getContent()).hasSize(1); + ReviewResponse item = res.getContent().getFirst(); + assertThat(item.getId()).isEqualTo(100L); + assertThat(item.getOverallRating()).isEqualTo(4.0); + } + + @Test + void listReviews_whenRatingFilterGiven_usesRatingQuery() { + Long workplaceId = 1L; + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(10L); + Profile profile = sampleProfile(user.getId()); + Review review = sampleReview(101L, wp, user); + + Page page = new PageImpl<>(List.of(review), PageRequest.of(0, 10), 1); + + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); + when(reviewRepository.findByWorkplace_IdAndOverallRatingIn( + eq(workplaceId), anyList(), any(Pageable.class))).thenReturn(page); + + when(reviewPolicyRatingRepository.findByReview_Id(101L)) + .thenReturn(List.of( + ReviewPolicyRating.builder() + .id(1L) + .review(review) + .policy(EthicalPolicy.SALARY_TRANSPARENCY) + .score(4) + .build())); + when(reviewReplyRepository.findByReview_Id(101L)) + .thenReturn(Optional.empty()); + when(profileRepository.findByUserId(user.getId())) + .thenReturn(Optional.of(profile)); + + PaginatedResponse res = reviewService.listReviews( + workplaceId, 0, 10, + "4", "rating", null, + null, null, null); + + assertThat(res).isNotNull(); + assertThat(res.getContent()).hasSize(1); + ReviewResponse item = res.getContent().getFirst(); + assertThat(item.getOverallRating()).isEqualTo(4.0); + verify(reviewRepository).findByWorkplace_IdAndOverallRatingIn(eq(workplaceId), anyList(), + any(Pageable.class)); + } + + @Test + void listReviews_whenRatingFilterRange_usesBetweenQuery() { + Long workplaceId = 1L; + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(10L); + Profile profile = sampleProfile(user.getId()); + Review review = sampleReview(103L, wp, user); + + Page page = new PageImpl<>(List.of(review), PageRequest.of(0, 10), 1); + + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); + when(reviewRepository.findByWorkplace_IdAndOverallRatingBetween( + eq(workplaceId), anyDouble(), anyDouble(), any(Pageable.class))).thenReturn(page); + + when(reviewPolicyRatingRepository.findByReview_Id(103L)) + .thenReturn(Collections.emptyList()); + when(reviewReplyRepository.findByReview_Id(103L)) + .thenReturn(Optional.empty()); + when(profileRepository.findByUserId(user.getId())) + .thenReturn(Optional.of(profile)); + + PaginatedResponse res = reviewService.listReviews( + workplaceId, 0, 10, + "3.5,4.5", "ratingDesc", null, + null, null, null); + + assertThat(res).isNotNull(); + assertThat(res.getContent()).hasSize(1); + verify(reviewRepository).findByWorkplace_IdAndOverallRatingBetween( + eq(workplaceId), anyDouble(), anyDouble(), any(Pageable.class)); + } + + @Test + void listReviews_whenWorkplaceNotFound_throwsHandleException() { + Long workplaceId = 1L; + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> reviewService.listReviews( + workplaceId, 0, 10, + null, null, null, + null, null, null)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.WORKPLACE_NOT_FOUND); + } + + @Test + void listReviews_whenHasCommentTrue_usesCommentQuery() { + Long workplaceId = 1L; + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(10L); + Profile profile = sampleProfile(user.getId()); + Review review = sampleReview(102L, wp, user); + review.setContent("Non-empty comment"); + + Page page = new PageImpl<>(List.of(review), PageRequest.of(0, 10), 1); + + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); + when(reviewRepository.findByWorkplace_IdAndContentIsNotNullAndContentNot( + eq(workplaceId), eq(""), any(Pageable.class))).thenReturn(page); + + when(reviewPolicyRatingRepository.findByReview_Id(102L)) + .thenReturn(Collections.emptyList()); + when(reviewReplyRepository.findByReview_Id(102L)) + .thenReturn(Optional.empty()); + when(profileRepository.findByUserId(user.getId())) + .thenReturn(Optional.of(profile)); + + PaginatedResponse res = reviewService.listReviews( + workplaceId, 0, 10, + null, null, true, + null, null, null); + + assertThat(res).isNotNull(); + assertThat(res.getContent()).hasSize(1); + ReviewResponse item = res.getContent().getFirst(); + assertThat(item.getContent()).isEqualTo("Non-empty comment"); + verify(reviewRepository).findByWorkplace_IdAndContentIsNotNullAndContentNot(eq(workplaceId), eq(""), + any(Pageable.class)); + } + + // ========== GET ONE ========== + + @Test + void getOne_whenValidRequest_returnsResponse() { + Long workplaceId = 1L; + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(10L); + Profile profile = sampleProfile(user.getId()); + Review review = sampleReview(200L, wp, user); + + when(reviewRepository.findById(200L)).thenReturn(Optional.of(review)); + when(reviewPolicyRatingRepository.findByReview_Id(200L)) + .thenReturn(Collections.emptyList()); + when(reviewReplyRepository.findByReview_Id(200L)) + .thenReturn(Optional.empty()); + when(profileRepository.findByUserId(user.getId())) + .thenReturn(Optional.of(profile)); + + ReviewResponse res = reviewService.getOne(workplaceId, 200L, null); + + assertThat(res).isNotNull(); + assertThat(res.getId()).isEqualTo(200L); + assertThat(res.getWorkplaceId()).isEqualTo(workplaceId); + } + + @Test + void getOne_whenWorkplaceMismatch_throwsHandleException() { + Long workplaceId = 1L; + Workplace wpOther = sampleWorkplace(2L); + User user = sampleUser(10L); + Review review = sampleReview(201L, wpOther, user); + + when(reviewRepository.findById(201L)).thenReturn(Optional.of(review)); + + assertThatThrownBy(() -> reviewService.getOne(workplaceId, 201L, null)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.REVIEW_NOT_FOUND); + } + + @Test + void getOne_whenReviewNotFound_throwsHandleException() { + Long workplaceId = 1L; + when(reviewRepository.findById(999L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> reviewService.getOne(workplaceId, 999L, null)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.REVIEW_NOT_FOUND); + } + + // ========== UPDATE REVIEW ========== + + @Test + void updateReview_whenOwnerUpdates_updatesFieldsAndPolicyRatings() { + Long workplaceId = 1L; + Long userId = 10L; + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(userId); + + Review existing = sampleReview(200L, wp, user); + existing.setOverallRating(3.0); + + when(reviewRepository.findById(200L)).thenReturn(Optional.of(existing)); + + ReviewPolicyRating rpr = ReviewPolicyRating.builder() .id(1L) - .review(null) + .review(existing) .policy(EthicalPolicy.SALARY_TRANSPARENCY) - .score(4) - .build() - )); - when(reviewReplyRepository.findByReview_Id(anyLong())) - .thenReturn(Optional.empty()); - when(profileRepository.findByUserId(userId)) - .thenReturn(Optional.of(profile)); - - ReviewResponse res = reviewService.createReview(workplaceId, req, user); - - assertThat(res).isNotNull(); - assertThat(res.getWorkplaceId()).isEqualTo(workplaceId); - assertThat(res.getTitle()).isEqualTo("Great workplace"); - assertThat(res.getOverallRating()).isEqualTo(4.0); - assertThat(res.getEthicalPolicyRatings()) - .containsEntry(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 4); - - verify(workplaceRepository).save(wp); - verify(reviewPolicyRatingRepository, times(1)).findByReview_Id(anyLong()); - } - - @Test - void createReview_whenEmployerOfWorkplace_throwsHandleException() { - Long workplaceId = 1L; - Long userId = 10L; - - Workplace wp = sampleWorkplace(workplaceId); - User user = sampleUser(userId); - - ReviewCreateRequest req = new ReviewCreateRequest(); - req.setEthicalPolicyRatings( - Map.of(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 4) - ); - req.setTitle("x"); - req.setContent("y"); - - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); - when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(true); - - assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.WORKPLACE_UNAUTHORIZED); - } - - @Test - void createReview_whenAlreadyReviewed_throwsHandleException() { - Long workplaceId = 1L; - Long userId = 10L; - - Workplace wp = sampleWorkplace(workplaceId); - User user = sampleUser(userId); - - ReviewCreateRequest req = new ReviewCreateRequest(); - req.setEthicalPolicyRatings( - Map.of(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 4) - ); - req.setTitle("x"); - req.setContent("y"); - - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); - when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(false); - when(reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(true); - - assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.REVIEW_ALREADY_EXISTS); - } - - @Test - void createReview_whenWorkplaceNotFound_throwsHandleException() { - Long workplaceId = 1L; - User user = sampleUser(10L); - ReviewCreateRequest req = new ReviewCreateRequest(); - - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.WORKPLACE_NOT_FOUND); - } - - @Test - void createReview_whenWorkplaceHasNoEthicalTags_throwsHandleException() { - Long workplaceId = 1L; - Long userId = 10L; - Workplace wp = sampleWorkplace(workplaceId); - wp.setEthicalTags(Collections.emptySet()); - User user = sampleUser(userId); - - ReviewCreateRequest req = new ReviewCreateRequest(); - req.setEthicalPolicyRatings( - Map.of(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 4) - ); - - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); - when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(false); - when(reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(false); - when(userRepository.findById(userId)).thenReturn(Optional.of(user)); - - assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.VALIDATION_ERROR); - } - - @Test - void createReview_whenPolicyRatingsNull_throwsHandleException() { - Long workplaceId = 1L; - Long userId = 10L; - Workplace wp = sampleWorkplace(workplaceId); - User user = sampleUser(userId); - - ReviewCreateRequest req = new ReviewCreateRequest(); - req.setEthicalPolicyRatings(null); - - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); - when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(false); - when(reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(false); - when(userRepository.findById(userId)).thenReturn(Optional.of(user)); - - assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.VALIDATION_ERROR); - } - - @Test - void createReview_whenScoreOutOfRange_throwsHandleException() { - Long workplaceId = 1L; - Long userId = 10L; - - Workplace wp = sampleWorkplace(workplaceId); - User user = sampleUser(userId); - - ReviewCreateRequest req = new ReviewCreateRequest(); - req.setTitle("Bad score"); - req.setContent("Test"); - req.setEthicalPolicyRatings( - Map.of(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 6) - ); - - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); - when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(false); - when(reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(false); - when(userRepository.findById(userId)).thenReturn(Optional.of(user)); - - assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.VALIDATION_ERROR); - } - - @Test - void createReview_whenUnknownPolicyLabel_throwsHandleException() { - Long workplaceId = 1L; - Long userId = 10L; - - Workplace wp = sampleWorkplace(workplaceId); - User user = sampleUser(userId); - - ReviewCreateRequest req = new ReviewCreateRequest(); - req.setTitle("Unknown policy"); - req.setContent("Test"); - req.setEthicalPolicyRatings( - Map.of("UnknownPolicy", 4) - ); - - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); - when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(false); - when(reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(false); - when(userRepository.findById(userId)).thenReturn(Optional.of(user)); - - assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.VALIDATION_ERROR); - } - - @Test - void createReview_whenPolicyNotDeclaredByWorkplace_throwsHandleException() { - Long workplaceId = 1L; - Long userId = 10L; - - Workplace wp = sampleWorkplace(workplaceId); - wp.setEthicalTags(Set.of(EthicalPolicy.EQUAL_PAY_POLICY)); - User user = sampleUser(userId); - - ReviewCreateRequest req = new ReviewCreateRequest(); - req.setTitle("Undeclared policy"); - req.setContent("Test"); - req.setEthicalPolicyRatings( - Map.of(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 4) - ); - - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); - when(employerWorkplaceRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(false); - when(reviewRepository.existsByWorkplace_IdAndUser_Id(workplaceId, userId)) - .thenReturn(false); - when(userRepository.findById(userId)).thenReturn(Optional.of(user)); - - assertThatThrownBy(() -> reviewService.createReview(workplaceId, req, user)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.VALIDATION_ERROR); - } - - // ========== LIST REVIEWS ========== - - @Test - void listReviews_whenNoFilters_returnsPaginatedReviews() { - Long workplaceId = 1L; - Workplace wp = sampleWorkplace(workplaceId); - User user = sampleUser(10L); - Profile profile = sampleProfile(user.getId()); - - Review review = sampleReview(100L, wp, user); - Page page = new PageImpl<>(List.of(review), PageRequest.of(0, 10), 1); - - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); - when(reviewRepository.findByWorkplace_Id(eq(workplaceId), any(Pageable.class))) - .thenReturn(page); - - when(reviewPolicyRatingRepository.findByReview_Id(100L)) - .thenReturn(List.of( - ReviewPolicyRating.builder() + .score(3) + .build(); + + when(reviewPolicyRatingRepository.findByReview_Id(200L)) + .thenReturn(List.of(rpr)); + + when(reviewRepository.save(any(Review.class))).thenAnswer(invocation -> { + Review r = invocation.getArgument(0); + if (r.getCreatedAt() == null) + r.setCreatedAt(Instant.now()); + if (r.getUpdatedAt() == null) + r.setUpdatedAt(Instant.now()); + return r; + }); + + when(reviewReplyRepository.findByReview_Id(200L)) + .thenReturn(Optional.empty()); + when(profileRepository.findByUserId(userId)) + .thenReturn(Optional.of(sampleProfile(userId))); + + ReviewUpdateRequest req = new ReviewUpdateRequest(); + req.setTitle("Updated title"); + req.setContent("Updated content"); + Map updatedPolicies = new HashMap<>(); + updatedPolicies.put(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 5); + req.setEthicalPolicyRatings(updatedPolicies); + + ReviewResponse res = reviewService.updateReview(workplaceId, 200L, req, user); + + assertThat(res.getTitle()).isEqualTo("Updated title"); + assertThat(res.getContent()).isEqualTo("Updated content"); + assertThat(res.getOverallRating()).isEqualTo(5.0); + + verify(reviewPolicyRatingRepository, atLeastOnce()).findByReview_Id(200L); + verify(reviewRepository, atLeastOnce()).save(existing); + } + + @Test + void updateReview_whenNotOwner_throwsHandleException() { + Long workplaceId = 1L; + Workplace wp = sampleWorkplace(workplaceId); + User owner = sampleUser(10L); + User other = sampleUser(99L); + + Review existing = sampleReview(300L, wp, owner); + + when(reviewRepository.findById(300L)).thenReturn(Optional.of(existing)); + + ReviewUpdateRequest req = new ReviewUpdateRequest(); + req.setTitle("Should not matter"); + + assertThatThrownBy(() -> reviewService.updateReview(workplaceId, 300L, req, other)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.ACCESS_DENIED); + } + + @Test + void updateReview_whenReviewNotFound_throwsHandleException() { + Long workplaceId = 1L; + User user = sampleUser(10L); + ReviewUpdateRequest req = new ReviewUpdateRequest(); + + when(reviewRepository.findById(999L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> reviewService.updateReview(workplaceId, 999L, req, user)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.REVIEW_NOT_FOUND); + } + + @Test + void updateReview_whenWorkplaceMismatch_throwsHandleException() { + Long workplaceId = 1L; + Workplace otherWp = sampleWorkplace(2L); + User user = sampleUser(10L); + Review review = sampleReview(500L, otherWp, user); + + when(reviewRepository.findById(500L)).thenReturn(Optional.of(review)); + + ReviewUpdateRequest req = new ReviewUpdateRequest(); + req.setTitle("test"); + + assertThatThrownBy(() -> reviewService.updateReview(workplaceId, 500L, req, user)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.REVIEW_NOT_FOUND); + } + + @Test + void updateReview_whenPolicyScoreOutOfRange_throwsHandleException() { + Long workplaceId = 1L; + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(10L); + Review review = sampleReview(600L, wp, user); + + when(reviewRepository.findById(600L)).thenReturn(Optional.of(review)); + when(reviewPolicyRatingRepository.findByReview_Id(600L)).thenReturn(Collections.emptyList()); + + ReviewUpdateRequest req = new ReviewUpdateRequest(); + req.setEthicalPolicyRatings( + Map.of(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 0)); + + assertThatThrownBy(() -> reviewService.updateReview(workplaceId, 600L, req, user)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.VALIDATION_ERROR); + } + + @Test + void updateReview_whenUnknownPolicyLabel_throwsHandleException() { + Long workplaceId = 1L; + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(10L); + Review review = sampleReview(601L, wp, user); + + when(reviewRepository.findById(601L)).thenReturn(Optional.of(review)); + when(reviewPolicyRatingRepository.findByReview_Id(601L)).thenReturn(Collections.emptyList()); + + ReviewUpdateRequest req = new ReviewUpdateRequest(); + req.setEthicalPolicyRatings( + Map.of("UnknownPolicy", 4)); + + assertThatThrownBy(() -> reviewService.updateReview(workplaceId, 601L, req, user)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.VALIDATION_ERROR); + } + + @Test + void updateReview_whenPolicyNotDeclaredByWorkplace_throwsHandleException() { + Long workplaceId = 1L; + Workplace wp = sampleWorkplace(workplaceId); + wp.setEthicalTags(Set.of(EthicalPolicy.EQUAL_PAY_POLICY)); + User user = sampleUser(10L); + Review review = sampleReview(602L, wp, user); + + when(reviewRepository.findById(602L)).thenReturn(Optional.of(review)); + when(reviewPolicyRatingRepository.findByReview_Id(602L)).thenReturn(Collections.emptyList()); + + ReviewUpdateRequest req = new ReviewUpdateRequest(); + req.setEthicalPolicyRatings( + Map.of(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 4)); + + assertThatThrownBy(() -> reviewService.updateReview(workplaceId, 602L, req, user)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.VALIDATION_ERROR); + } + + // ========== DELETE REVIEW ========== + + @Test + void deleteReview_whenOwnerDeletes_removesReviewAndRelatedEntities() { + Long workplaceId = 1L; + Long userId = 10L; + + Workplace wp = sampleWorkplace(workplaceId); + wp.setReviewCount(1L); + User user = sampleUser(userId); + Review review = sampleReview(300L, wp, user); + + ReviewReply reply = ReviewReply.builder() .id(1L) .review(review) + .content("Thanks for feedback") + .build(); + + ReviewPolicyRating rating1 = ReviewPolicyRating.builder() + .id(10L) + .review(review) .policy(EthicalPolicy.SALARY_TRANSPARENCY) .score(4) - .build() - )); - when(reviewReplyRepository.findByReview_Id(100L)) - .thenReturn(Optional.empty()); - when(profileRepository.findByUserId(user.getId())) - .thenReturn(Optional.of(profile)); - - PaginatedResponse res = reviewService.listReviews( - workplaceId, 0, 10, - null, null, null, - null, null - ); - - assertThat(res).isNotNull(); - assertThat(res.getContent()).hasSize(1); - ReviewResponse item = res.getContent().getFirst(); - assertThat(item.getId()).isEqualTo(100L); - assertThat(item.getOverallRating()).isEqualTo(4.0); - } - - @Test - void listReviews_whenRatingFilterGiven_usesRatingQuery() { - Long workplaceId = 1L; - Workplace wp = sampleWorkplace(workplaceId); - User user = sampleUser(10L); - Profile profile = sampleProfile(user.getId()); - Review review = sampleReview(101L, wp, user); - - Page page = new PageImpl<>(List.of(review), PageRequest.of(0, 10), 1); - - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); - when(reviewRepository.findByWorkplace_IdAndOverallRatingIn( - eq(workplaceId), anyList(), any(Pageable.class) - )).thenReturn(page); - - when(reviewPolicyRatingRepository.findByReview_Id(101L)) - .thenReturn(List.of( - ReviewPolicyRating.builder() + .build(); + + when(reviewRepository.findById(300L)).thenReturn(Optional.of(review)); + when(reviewReplyRepository.findByReview_Id(300L)).thenReturn(Optional.of(reply)); + when(reviewPolicyRatingRepository.findByReview_Id(300L)) + .thenReturn(List.of(rating1)); + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); + + reviewService.deleteReview(workplaceId, 300L, user, false); + + verify(reviewReplyRepository).delete(reply); + verify(reviewPolicyRatingRepository).deleteAll(List.of(rating1)); + verify(reviewRepository).delete(review); + verify(workplaceRepository).save(wp); + assertThat(wp.getReviewCount()).isGreaterThanOrEqualTo(0L); + } + + @Test + void deleteReview_whenNotOwnerAndNotAdmin_throwsHandleException() { + Long workplaceId = 1L; + Workplace wp = sampleWorkplace(workplaceId); + User owner = sampleUser(10L); + User other = sampleUser(99L); + Review review = sampleReview(301L, wp, owner); + + when(reviewRepository.findById(301L)).thenReturn(Optional.of(review)); + + assertThatThrownBy(() -> reviewService.deleteReview(workplaceId, 301L, other, false)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.ACCESS_DENIED); + } + + // ========== TOGGLE HELPFUL ========== + + @Test + void toggleHelpful_whenNotHelpfulBefore_createsReactionAndIncrementsCount() { + Long workplaceId = 1L; + Long userId = 10L; + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(userId); + User otherUser = sampleUser(99L); + + Review review = sampleReview(100L, wp, otherUser); + review.setHelpfulCount(5); + + when(reviewRepository.findById(100L)).thenReturn(Optional.of(review)); + when(reviewReactionRepository.findByReview_IdAndUser_Id(100L, userId)) + .thenReturn(Optional.empty()); + + ReviewResponse res = reviewService.toggleHelpful(workplaceId, 100L, user); + + assertThat(res.getHelpfulCount()).isEqualTo(6); + assertThat(res.isHelpfulByUser()).isTrue(); + + verify(reviewReactionRepository).save(any(ReviewReaction.class)); + verify(reviewRepository).save(review); + } + + @Test + void toggleHelpful_whenAlreadyHelpful_deletesReactionAndDecrementsCount() { + Long workplaceId = 1L; + Long userId = 10L; + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(userId); + User otherUser = sampleUser(99L); + + Review review = sampleReview(100L, wp, otherUser); + review.setHelpfulCount(5); + + ReviewReaction reaction = ReviewReaction.builder() .id(1L) .review(review) + .user(user) + .build(); + + when(reviewRepository.findById(100L)).thenReturn(Optional.of(review)); + when(reviewReactionRepository.findByReview_IdAndUser_Id(100L, userId)) + .thenReturn(Optional.of(reaction)); + + ReviewResponse res = reviewService.toggleHelpful(workplaceId, 100L, user); + + assertThat(res.getHelpfulCount()).isEqualTo(4); + assertThat(res.isHelpfulByUser()).isFalse(); + + verify(reviewReactionRepository).delete(reaction); + verify(reviewRepository).save(review); + } + + @Test + void toggleHelpful_whenOwnReview_throwsHandleException() { + Long workplaceId = 1L; + Long userId = 10L; + Workplace wp = sampleWorkplace(workplaceId); + User user = sampleUser(userId); + + Review review = sampleReview(100L, wp, user); + when(reviewRepository.findById(100L)).thenReturn(Optional.of(review)); + + assertThatThrownBy(() -> reviewService.toggleHelpful(workplaceId, 100L, user)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.VALIDATION_ERROR); + + verify(reviewReactionRepository, never()).save(any()); + verify(reviewReactionRepository, never()).delete(any()); + } + + @Test + void deleteReview_whenReviewNotFound_throwsHandleException() { + Long workplaceId = 1L; + User user = sampleUser(10L); + + when(reviewRepository.findById(999L)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> reviewService.deleteReview(workplaceId, 999L, user, false)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.REVIEW_NOT_FOUND); + } + + @Test + void deleteReview_whenWorkplaceMismatch_throwsHandleException() { + Long workplaceId = 1L; + Workplace otherWp = sampleWorkplace(2L); + User user = sampleUser(10L); + Review review = sampleReview(700L, otherWp, user); + + when(reviewRepository.findById(700L)).thenReturn(Optional.of(review)); + + assertThatThrownBy(() -> reviewService.deleteReview(workplaceId, 700L, user, false)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.REVIEW_NOT_FOUND); + } + + @Test + void deleteReview_whenWorkplaceNotFoundWhileUpdatingCount_throwsHandleException() { + Long workplaceId = 1L; + User user = sampleUser(10L); + Workplace wp = sampleWorkplace(workplaceId); + Review review = sampleReview(701L, wp, user); + + when(reviewRepository.findById(701L)).thenReturn(Optional.of(review)); + when(reviewReplyRepository.findByReview_Id(701L)).thenReturn(Optional.empty()); + when(reviewPolicyRatingRepository.findByReview_Id(701L)).thenReturn(Collections.emptyList()); + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.empty()); + + assertThatThrownBy(() -> reviewService.deleteReview(workplaceId, 701L, user, false)) + .isInstanceOf(HandleException.class) + .extracting("code") + .isEqualTo(ErrorCode.WORKPLACE_NOT_FOUND); + } + + @Test + void deleteReview_whenAdminDeletes_removesReviewAndRelatedEntities() { + Long workplaceId = 1L; + Workplace wp = sampleWorkplace(workplaceId); + wp.setReviewCount(1L); + User owner = sampleUser(10L); + User admin = sampleUser(99L); + Review review = sampleReview(401L, wp, owner); + + ReviewReply reply = ReviewReply.builder() + .id(2L) + .review(review) + .content("Admin reply") + .build(); + + ReviewPolicyRating rating = ReviewPolicyRating.builder() + .id(11L) + .review(review) .policy(EthicalPolicy.SALARY_TRANSPARENCY) - .score(4) - .build() - )); - when(reviewReplyRepository.findByReview_Id(101L)) - .thenReturn(Optional.empty()); - when(profileRepository.findByUserId(user.getId())) - .thenReturn(Optional.of(profile)); - - PaginatedResponse res = reviewService.listReviews( - workplaceId, 0, 10, - "4", "rating", null, - null, null - ); - - assertThat(res).isNotNull(); - assertThat(res.getContent()).hasSize(1); - ReviewResponse item = res.getContent().getFirst(); - assertThat(item.getOverallRating()).isEqualTo(4.0); - verify(reviewRepository).findByWorkplace_IdAndOverallRatingIn(eq(workplaceId), anyList(), any(Pageable.class)); - } - - @Test - void listReviews_whenRatingFilterRange_usesBetweenQuery() { - Long workplaceId = 1L; - Workplace wp = sampleWorkplace(workplaceId); - User user = sampleUser(10L); - Profile profile = sampleProfile(user.getId()); - Review review = sampleReview(103L, wp, user); - - Page page = new PageImpl<>(List.of(review), PageRequest.of(0, 10), 1); - - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); - when(reviewRepository.findByWorkplace_IdAndOverallRatingBetween( - eq(workplaceId), anyDouble(), anyDouble(), any(Pageable.class) - )).thenReturn(page); - - when(reviewPolicyRatingRepository.findByReview_Id(103L)) - .thenReturn(Collections.emptyList()); - when(reviewReplyRepository.findByReview_Id(103L)) - .thenReturn(Optional.empty()); - when(profileRepository.findByUserId(user.getId())) - .thenReturn(Optional.of(profile)); - - PaginatedResponse res = reviewService.listReviews( - workplaceId, 0, 10, - "3.5,4.5", "ratingDesc", null, - null, null - ); - - assertThat(res).isNotNull(); - assertThat(res.getContent()).hasSize(1); - verify(reviewRepository).findByWorkplace_IdAndOverallRatingBetween( - eq(workplaceId), anyDouble(), anyDouble(), any(Pageable.class) - ); - } - - @Test - void listReviews_whenWorkplaceNotFound_throwsHandleException() { - Long workplaceId = 1L; - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> reviewService.listReviews( - workplaceId, 0, 10, - null, null, null, - null, null - )) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.WORKPLACE_NOT_FOUND); - } - - @Test - void listReviews_whenHasCommentTrue_usesCommentQuery() { - Long workplaceId = 1L; - Workplace wp = sampleWorkplace(workplaceId); - User user = sampleUser(10L); - Profile profile = sampleProfile(user.getId()); - Review review = sampleReview(102L, wp, user); - review.setContent("Non-empty comment"); - - Page page = new PageImpl<>(List.of(review), PageRequest.of(0, 10), 1); - - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); - when(reviewRepository.findByWorkplace_IdAndContentIsNotNullAndContentNot( - eq(workplaceId), eq(""), any(Pageable.class) - )).thenReturn(page); - - when(reviewPolicyRatingRepository.findByReview_Id(102L)) - .thenReturn(Collections.emptyList()); - when(reviewReplyRepository.findByReview_Id(102L)) - .thenReturn(Optional.empty()); - when(profileRepository.findByUserId(user.getId())) - .thenReturn(Optional.of(profile)); - - PaginatedResponse res = reviewService.listReviews( - workplaceId, 0, 10, - null, null, true, - null, null - ); - - assertThat(res).isNotNull(); - assertThat(res.getContent()).hasSize(1); - ReviewResponse item = res.getContent().getFirst(); - assertThat(item.getContent()).isEqualTo("Non-empty comment"); - verify(reviewRepository).findByWorkplace_IdAndContentIsNotNullAndContentNot(eq(workplaceId), eq(""), any(Pageable.class)); - } - - // ========== GET ONE ========== - - @Test - void getOne_whenValidRequest_returnsResponse() { - Long workplaceId = 1L; - Workplace wp = sampleWorkplace(workplaceId); - User user = sampleUser(10L); - Profile profile = sampleProfile(user.getId()); - Review review = sampleReview(200L, wp, user); - - when(reviewRepository.findById(200L)).thenReturn(Optional.of(review)); - when(reviewPolicyRatingRepository.findByReview_Id(200L)) - .thenReturn(Collections.emptyList()); - when(reviewReplyRepository.findByReview_Id(200L)) - .thenReturn(Optional.empty()); - when(profileRepository.findByUserId(user.getId())) - .thenReturn(Optional.of(profile)); - - ReviewResponse res = reviewService.getOne(workplaceId, 200L); - - assertThat(res).isNotNull(); - assertThat(res.getId()).isEqualTo(200L); - assertThat(res.getWorkplaceId()).isEqualTo(workplaceId); - } - - @Test - void getOne_whenWorkplaceMismatch_throwsHandleException() { - Long workplaceId = 1L; - Workplace wpOther = sampleWorkplace(2L); - User user = sampleUser(10L); - Review review = sampleReview(201L, wpOther, user); - - when(reviewRepository.findById(201L)).thenReturn(Optional.of(review)); - - assertThatThrownBy(() -> reviewService.getOne(workplaceId, 201L)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.REVIEW_NOT_FOUND); - } - - @Test - void getOne_whenReviewNotFound_throwsHandleException() { - Long workplaceId = 1L; - when(reviewRepository.findById(999L)).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> reviewService.getOne(workplaceId, 999L)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.REVIEW_NOT_FOUND); - } - - // ========== UPDATE REVIEW ========== - - @Test - void updateReview_whenOwnerUpdates_updatesFieldsAndPolicyRatings() { - Long workplaceId = 1L; - Long userId = 10L; - Workplace wp = sampleWorkplace(workplaceId); - User user = sampleUser(userId); - - Review existing = sampleReview(200L, wp, user); - existing.setOverallRating(3.0); - - when(reviewRepository.findById(200L)).thenReturn(Optional.of(existing)); - - ReviewPolicyRating rpr = ReviewPolicyRating.builder() - .id(1L) - .review(existing) - .policy(EthicalPolicy.SALARY_TRANSPARENCY) - .score(3) - .build(); - - when(reviewPolicyRatingRepository.findByReview_Id(200L)) - .thenReturn(List.of(rpr)); - - when(reviewRepository.save(any(Review.class))).thenAnswer(invocation -> { - Review r = invocation.getArgument(0); - if (r.getCreatedAt() == null) r.setCreatedAt(Instant.now()); - if (r.getUpdatedAt() == null) r.setUpdatedAt(Instant.now()); - return r; - }); - - when(reviewReplyRepository.findByReview_Id(200L)) - .thenReturn(Optional.empty()); - when(profileRepository.findByUserId(userId)) - .thenReturn(Optional.of(sampleProfile(userId))); - - ReviewUpdateRequest req = new ReviewUpdateRequest(); - req.setTitle("Updated title"); - req.setContent("Updated content"); - Map updatedPolicies = new HashMap<>(); - updatedPolicies.put(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 5); - req.setEthicalPolicyRatings(updatedPolicies); - - ReviewResponse res = reviewService.updateReview(workplaceId, 200L, req, user); - - assertThat(res.getTitle()).isEqualTo("Updated title"); - assertThat(res.getContent()).isEqualTo("Updated content"); - assertThat(res.getOverallRating()).isEqualTo(5.0); - - verify(reviewPolicyRatingRepository, atLeastOnce()).findByReview_Id(200L); - verify(reviewRepository, atLeastOnce()).save(existing); - } - - @Test - void updateReview_whenNotOwner_throwsHandleException() { - Long workplaceId = 1L; - Workplace wp = sampleWorkplace(workplaceId); - User owner = sampleUser(10L); - User other = sampleUser(99L); - - Review existing = sampleReview(300L, wp, owner); - - when(reviewRepository.findById(300L)).thenReturn(Optional.of(existing)); - - ReviewUpdateRequest req = new ReviewUpdateRequest(); - req.setTitle("Should not matter"); - - assertThatThrownBy(() -> reviewService.updateReview(workplaceId, 300L, req, other)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.ACCESS_DENIED); - } - - @Test - void updateReview_whenReviewNotFound_throwsHandleException() { - Long workplaceId = 1L; - User user = sampleUser(10L); - ReviewUpdateRequest req = new ReviewUpdateRequest(); - - when(reviewRepository.findById(999L)).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> reviewService.updateReview(workplaceId, 999L, req, user)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.REVIEW_NOT_FOUND); - } - - @Test - void updateReview_whenWorkplaceMismatch_throwsHandleException() { - Long workplaceId = 1L; - Workplace otherWp = sampleWorkplace(2L); - User user = sampleUser(10L); - Review review = sampleReview(500L, otherWp, user); - - when(reviewRepository.findById(500L)).thenReturn(Optional.of(review)); - - ReviewUpdateRequest req = new ReviewUpdateRequest(); - req.setTitle("test"); - - assertThatThrownBy(() -> reviewService.updateReview(workplaceId, 500L, req, user)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.REVIEW_NOT_FOUND); - } - - @Test - void updateReview_whenPolicyScoreOutOfRange_throwsHandleException() { - Long workplaceId = 1L; - Workplace wp = sampleWorkplace(workplaceId); - User user = sampleUser(10L); - Review review = sampleReview(600L, wp, user); - - when(reviewRepository.findById(600L)).thenReturn(Optional.of(review)); - when(reviewPolicyRatingRepository.findByReview_Id(600L)).thenReturn(Collections.emptyList()); - - ReviewUpdateRequest req = new ReviewUpdateRequest(); - req.setEthicalPolicyRatings( - Map.of(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 0) - ); - - assertThatThrownBy(() -> reviewService.updateReview(workplaceId, 600L, req, user)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.VALIDATION_ERROR); - } - - @Test - void updateReview_whenUnknownPolicyLabel_throwsHandleException() { - Long workplaceId = 1L; - Workplace wp = sampleWorkplace(workplaceId); - User user = sampleUser(10L); - Review review = sampleReview(601L, wp, user); - - when(reviewRepository.findById(601L)).thenReturn(Optional.of(review)); - when(reviewPolicyRatingRepository.findByReview_Id(601L)).thenReturn(Collections.emptyList()); - - ReviewUpdateRequest req = new ReviewUpdateRequest(); - req.setEthicalPolicyRatings( - Map.of("UnknownPolicy", 4) - ); - - assertThatThrownBy(() -> reviewService.updateReview(workplaceId, 601L, req, user)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.VALIDATION_ERROR); - } - - @Test - void updateReview_whenPolicyNotDeclaredByWorkplace_throwsHandleException() { - Long workplaceId = 1L; - Workplace wp = sampleWorkplace(workplaceId); - wp.setEthicalTags(Set.of(EthicalPolicy.EQUAL_PAY_POLICY)); - User user = sampleUser(10L); - Review review = sampleReview(602L, wp, user); - - when(reviewRepository.findById(602L)).thenReturn(Optional.of(review)); - when(reviewPolicyRatingRepository.findByReview_Id(602L)).thenReturn(Collections.emptyList()); - - ReviewUpdateRequest req = new ReviewUpdateRequest(); - req.setEthicalPolicyRatings( - Map.of(EthicalPolicy.SALARY_TRANSPARENCY.getLabel(), 4) - ); - - assertThatThrownBy(() -> reviewService.updateReview(workplaceId, 602L, req, user)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.VALIDATION_ERROR); - } - - // ========== DELETE REVIEW ========== - - @Test - void deleteReview_whenOwnerDeletes_removesReviewAndRelatedEntities() { - Long workplaceId = 1L; - Long userId = 10L; - - Workplace wp = sampleWorkplace(workplaceId); - wp.setReviewCount(1L); - User user = sampleUser(userId); - Review review = sampleReview(300L, wp, user); - - ReviewReply reply = ReviewReply.builder() - .id(1L) - .review(review) - .content("Thanks for feedback") - .build(); - - ReviewPolicyRating rating1 = ReviewPolicyRating.builder() - .id(10L) - .review(review) - .policy(EthicalPolicy.SALARY_TRANSPARENCY) - .score(4) - .build(); - - when(reviewRepository.findById(300L)).thenReturn(Optional.of(review)); - when(reviewReplyRepository.findByReview_Id(300L)).thenReturn(Optional.of(reply)); - when(reviewPolicyRatingRepository.findByReview_Id(300L)) - .thenReturn(List.of(rating1)); - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); - - reviewService.deleteReview(workplaceId, 300L, user, false); - - verify(reviewReplyRepository).delete(reply); - verify(reviewPolicyRatingRepository).deleteAll(List.of(rating1)); - verify(reviewRepository).delete(review); - verify(workplaceRepository).save(wp); - assertThat(wp.getReviewCount()).isGreaterThanOrEqualTo(0L); - } - - @Test - void deleteReview_whenNotOwnerAndNotAdmin_throwsHandleException() { - Long workplaceId = 1L; - Workplace wp = sampleWorkplace(workplaceId); - User owner = sampleUser(10L); - User other = sampleUser(99L); - Review review = sampleReview(400L, wp, owner); - - when(reviewRepository.findById(400L)).thenReturn(Optional.of(review)); - - assertThatThrownBy(() -> reviewService.deleteReview(workplaceId, 400L, other, false)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.ACCESS_DENIED); - } - - @Test - void deleteReview_whenReviewNotFound_throwsHandleException() { - Long workplaceId = 1L; - User user = sampleUser(10L); - - when(reviewRepository.findById(999L)).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> reviewService.deleteReview(workplaceId, 999L, user, false)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.REVIEW_NOT_FOUND); - } - - @Test - void deleteReview_whenWorkplaceMismatch_throwsHandleException() { - Long workplaceId = 1L; - Workplace otherWp = sampleWorkplace(2L); - User user = sampleUser(10L); - Review review = sampleReview(700L, otherWp, user); - - when(reviewRepository.findById(700L)).thenReturn(Optional.of(review)); - - assertThatThrownBy(() -> reviewService.deleteReview(workplaceId, 700L, user, false)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.REVIEW_NOT_FOUND); - } - - @Test - void deleteReview_whenWorkplaceNotFoundWhileUpdatingCount_throwsHandleException() { - Long workplaceId = 1L; - User user = sampleUser(10L); - Workplace wp = sampleWorkplace(workplaceId); - Review review = sampleReview(701L, wp, user); - - when(reviewRepository.findById(701L)).thenReturn(Optional.of(review)); - when(reviewReplyRepository.findByReview_Id(701L)).thenReturn(Optional.empty()); - when(reviewPolicyRatingRepository.findByReview_Id(701L)).thenReturn(Collections.emptyList()); - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.empty()); - - assertThatThrownBy(() -> reviewService.deleteReview(workplaceId, 701L, user, false)) - .isInstanceOf(HandleException.class) - .extracting("code") - .isEqualTo(ErrorCode.WORKPLACE_NOT_FOUND); - } - - @Test - void deleteReview_whenAdminDeletes_removesReviewAndRelatedEntities() { - Long workplaceId = 1L; - Workplace wp = sampleWorkplace(workplaceId); - wp.setReviewCount(1L); - User owner = sampleUser(10L); - User admin = sampleUser(99L); - Review review = sampleReview(401L, wp, owner); - - ReviewReply reply = ReviewReply.builder() - .id(2L) - .review(review) - .content("Admin reply") - .build(); - - ReviewPolicyRating rating = ReviewPolicyRating.builder() - .id(11L) - .review(review) - .policy(EthicalPolicy.SALARY_TRANSPARENCY) - .score(5) - .build(); - - when(reviewRepository.findById(401L)).thenReturn(Optional.of(review)); - when(reviewReplyRepository.findByReview_Id(401L)).thenReturn(Optional.of(reply)); - when(reviewPolicyRatingRepository.findByReview_Id(401L)) - .thenReturn(List.of(rating)); - when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); - - reviewService.deleteReview(workplaceId, 401L, admin, true); - - verify(reviewReplyRepository).delete(reply); - verify(reviewPolicyRatingRepository).deleteAll(List.of(rating)); - verify(reviewRepository).delete(review); - verify(workplaceRepository).save(wp); - assertThat(wp.getReviewCount()).isGreaterThanOrEqualTo(0L); - } + .score(5) + .build(); + + when(reviewRepository.findById(401L)).thenReturn(Optional.of(review)); + when(reviewReplyRepository.findByReview_Id(401L)).thenReturn(Optional.of(reply)); + when(reviewPolicyRatingRepository.findByReview_Id(401L)) + .thenReturn(List.of(rating)); + when(workplaceRepository.findById(workplaceId)).thenReturn(Optional.of(wp)); + + reviewService.deleteReview(workplaceId, 401L, admin, true); + + verify(reviewReplyRepository).delete(reply); + verify(reviewPolicyRatingRepository).deleteAll(List.of(rating)); + verify(reviewRepository).delete(review); + verify(workplaceRepository).save(wp); + assertThat(wp.getReviewCount()).isGreaterThanOrEqualTo(0L); + } } \ No newline at end of file