feat(backend): add V17 migration, @mention storage, MentionDTO, user search endpoint, and tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -39,7 +39,7 @@ public class CommentController {
|
|||||||
@RequestBody CreateCommentDTO dto,
|
@RequestBody CreateCommentDTO dto,
|
||||||
Authentication authentication) {
|
Authentication authentication) {
|
||||||
AppUser author = resolveUser(authentication);
|
AppUser author = resolveUser(authentication);
|
||||||
return commentService.postComment(documentId, null, dto.getContent(), author);
|
return commentService.postComment(documentId, null, dto.getContent(), dto.getMentionedUserIds(), author);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/api/documents/{documentId}/comments/{commentId}/replies")
|
@PostMapping("/api/documents/{documentId}/comments/{commentId}/replies")
|
||||||
@@ -51,7 +51,7 @@ public class CommentController {
|
|||||||
@RequestBody CreateCommentDTO dto,
|
@RequestBody CreateCommentDTO dto,
|
||||||
Authentication authentication) {
|
Authentication authentication) {
|
||||||
AppUser author = resolveUser(authentication);
|
AppUser author = resolveUser(authentication);
|
||||||
return commentService.replyToComment(documentId, commentId, dto.getContent(), author);
|
return commentService.replyToComment(documentId, commentId, dto.getContent(), dto.getMentionedUserIds(), author);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Annotation comments ──────────────────────────────────────────────────
|
// ─── Annotation comments ──────────────────────────────────────────────────
|
||||||
@@ -70,7 +70,7 @@ public class CommentController {
|
|||||||
@RequestBody CreateCommentDTO dto,
|
@RequestBody CreateCommentDTO dto,
|
||||||
Authentication authentication) {
|
Authentication authentication) {
|
||||||
AppUser author = resolveUser(authentication);
|
AppUser author = resolveUser(authentication);
|
||||||
return commentService.postComment(documentId, annotationId, dto.getContent(), author);
|
return commentService.postComment(documentId, annotationId, dto.getContent(), dto.getMentionedUserIds(), author);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/api/documents/{documentId}/annotations/{annotationId}/comments/{commentId}/replies")
|
@PostMapping("/api/documents/{documentId}/annotations/{annotationId}/comments/{commentId}/replies")
|
||||||
@@ -82,7 +82,7 @@ public class CommentController {
|
|||||||
@RequestBody CreateCommentDTO dto,
|
@RequestBody CreateCommentDTO dto,
|
||||||
Authentication authentication) {
|
Authentication authentication) {
|
||||||
AppUser author = resolveUser(authentication);
|
AppUser author = resolveUser(authentication);
|
||||||
return commentService.replyToComment(documentId, commentId, dto.getContent(), author);
|
return commentService.replyToComment(documentId, commentId, dto.getContent(), dto.getMentionedUserIds(), author);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Edit and delete (shared) ─────────────────────────────────────────────
|
// ─── Edit and delete (shared) ─────────────────────────────────────────────
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package org.raddatz.familienarchiv.controller;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.raddatz.familienarchiv.dto.MentionDTO;
|
||||||
|
import org.raddatz.familienarchiv.model.AppUser;
|
||||||
|
import org.raddatz.familienarchiv.service.UserSearchService;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserSearchController {
|
||||||
|
|
||||||
|
private final UserSearchService userSearchService;
|
||||||
|
|
||||||
|
@GetMapping("/api/users/search")
|
||||||
|
public List<MentionDTO> search(@RequestParam(defaultValue = "") String q) {
|
||||||
|
return userSearchService.search(q).stream()
|
||||||
|
.map(this::toMentionDTO)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MentionDTO toMentionDTO(AppUser user) {
|
||||||
|
return new MentionDTO(user.getId(), user.getFirstName(), user.getLastName());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,12 @@ package org.raddatz.familienarchiv.dto;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class CreateCommentDTO {
|
public class CreateCommentDTO {
|
||||||
private String content;
|
private String content;
|
||||||
|
private List<UUID> mentionedUserIds = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package org.raddatz.familienarchiv.dto;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
public record MentionDTO(
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) UUID id,
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String firstName,
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED) String lastName
|
||||||
|
) {}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
package org.raddatz.familienarchiv.model;
|
package org.raddatz.familienarchiv.model;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.*;
|
import lombok.*;
|
||||||
import org.hibernate.annotations.CreationTimestamp;
|
import org.hibernate.annotations.CreationTimestamp;
|
||||||
import org.hibernate.annotations.UpdateTimestamp;
|
import org.hibernate.annotations.UpdateTimestamp;
|
||||||
|
import org.raddatz.familienarchiv.dto.MentionDTO;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -60,4 +62,21 @@ public class DocumentComment {
|
|||||||
@Builder.Default
|
@Builder.Default
|
||||||
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private List<DocumentComment> replies = new ArrayList<>();
|
private List<DocumentComment> replies = new ArrayList<>();
|
||||||
|
|
||||||
|
// JPA join table for structured mention references — not serialized directly
|
||||||
|
@ManyToMany(fetch = FetchType.LAZY)
|
||||||
|
@JoinTable(
|
||||||
|
name = "comment_mentions",
|
||||||
|
joinColumns = @JoinColumn(name = "comment_id"),
|
||||||
|
inverseJoinColumns = @JoinColumn(name = "user_id")
|
||||||
|
)
|
||||||
|
@JsonIgnore
|
||||||
|
@Builder.Default
|
||||||
|
private List<AppUser> mentions = new ArrayList<>();
|
||||||
|
|
||||||
|
// Populated by CommentService before serialization — not persisted.
|
||||||
|
@Transient
|
||||||
|
@Builder.Default
|
||||||
|
@Schema(requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private List<MentionDTO> mentionDTOs = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package org.raddatz.familienarchiv.repository;
|
package org.raddatz.familienarchiv.repository;
|
||||||
|
|
||||||
|
|
||||||
import org.raddatz.familienarchiv.model.AppUser;
|
import org.raddatz.familienarchiv.model.AppUser;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import org.springframework.data.jpa.repository.Query;
|
||||||
|
import org.springframework.data.repository.query.Param;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
@@ -12,4 +15,9 @@ import java.util.UUID;
|
|||||||
public interface AppUserRepository extends JpaRepository<AppUser, UUID> {
|
public interface AppUserRepository extends JpaRepository<AppUser, UUID> {
|
||||||
Optional<AppUser> findByUsername(String username);
|
Optional<AppUser> findByUsername(String username);
|
||||||
Optional<AppUser> findByEmail(String email);
|
Optional<AppUser> findByEmail(String email);
|
||||||
|
|
||||||
|
@Query("SELECT u FROM AppUser u WHERE " +
|
||||||
|
"LOWER(COALESCE(u.firstName, '') || ' ' || COALESCE(u.lastName, '')) LIKE LOWER(CONCAT('%', :q, '%')) " +
|
||||||
|
"OR LOWER(u.username) LIKE LOWER(CONCAT('%', :q, '%'))")
|
||||||
|
List<AppUser> searchByNameOrUsername(@Param("q") String q, Pageable pageable);
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
package org.raddatz.familienarchiv.service;
|
package org.raddatz.familienarchiv.service;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.raddatz.familienarchiv.dto.MentionDTO;
|
||||||
import org.raddatz.familienarchiv.exception.DomainException;
|
import org.raddatz.familienarchiv.exception.DomainException;
|
||||||
import org.raddatz.familienarchiv.exception.ErrorCode;
|
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||||
import org.raddatz.familienarchiv.model.AppUser;
|
import org.raddatz.familienarchiv.model.AppUser;
|
||||||
import org.raddatz.familienarchiv.model.DocumentComment;
|
import org.raddatz.familienarchiv.model.DocumentComment;
|
||||||
|
import org.raddatz.familienarchiv.repository.AppUserRepository;
|
||||||
import org.raddatz.familienarchiv.repository.CommentRepository;
|
import org.raddatz.familienarchiv.repository.CommentRepository;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
@@ -17,21 +19,23 @@ import java.util.UUID;
|
|||||||
public class CommentService {
|
public class CommentService {
|
||||||
|
|
||||||
private final CommentRepository commentRepository;
|
private final CommentRepository commentRepository;
|
||||||
|
private final AppUserRepository userRepository;
|
||||||
private final NotificationService notificationService;
|
private final NotificationService notificationService;
|
||||||
|
|
||||||
public List<DocumentComment> getCommentsForDocument(UUID documentId) {
|
public List<DocumentComment> getCommentsForDocument(UUID documentId) {
|
||||||
List<DocumentComment> roots =
|
List<DocumentComment> roots =
|
||||||
commentRepository.findByDocumentIdAndAnnotationIdIsNullAndParentIdIsNull(documentId);
|
commentRepository.findByDocumentIdAndAnnotationIdIsNullAndParentIdIsNull(documentId);
|
||||||
return withReplies(roots);
|
return withRepliesAndMentions(roots);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<DocumentComment> getCommentsForAnnotation(UUID annotationId) {
|
public List<DocumentComment> getCommentsForAnnotation(UUID annotationId) {
|
||||||
List<DocumentComment> roots = commentRepository.findByAnnotationIdAndParentIdIsNull(annotationId);
|
List<DocumentComment> roots = commentRepository.findByAnnotationIdAndParentIdIsNull(annotationId);
|
||||||
return withReplies(roots);
|
return withRepliesAndMentions(roots);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public DocumentComment postComment(UUID documentId, UUID annotationId, String content, AppUser author) {
|
public DocumentComment postComment(UUID documentId, UUID annotationId, String content,
|
||||||
|
List<UUID> mentionedUserIds, AppUser author) {
|
||||||
DocumentComment comment = DocumentComment.builder()
|
DocumentComment comment = DocumentComment.builder()
|
||||||
.documentId(documentId)
|
.documentId(documentId)
|
||||||
.annotationId(annotationId)
|
.annotationId(annotationId)
|
||||||
@@ -39,11 +43,16 @@ public class CommentService {
|
|||||||
.authorId(author.getId())
|
.authorId(author.getId())
|
||||||
.authorName(resolveAuthorName(author))
|
.authorName(resolveAuthorName(author))
|
||||||
.build();
|
.build();
|
||||||
return commentRepository.save(comment);
|
saveMentions(comment, mentionedUserIds);
|
||||||
|
DocumentComment saved = commentRepository.save(comment);
|
||||||
|
withMentionDTOs(saved);
|
||||||
|
notificationService.notifyMentions(mentionedUserIds, saved);
|
||||||
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public DocumentComment replyToComment(UUID documentId, UUID commentId, String content, AppUser author) {
|
public DocumentComment replyToComment(UUID documentId, UUID commentId, String content,
|
||||||
|
List<UUID> mentionedUserIds, AppUser author) {
|
||||||
DocumentComment target = commentRepository.findById(commentId)
|
DocumentComment target = commentRepository.findById(commentId)
|
||||||
.orElseThrow(() -> DomainException.notFound(
|
.orElseThrow(() -> DomainException.notFound(
|
||||||
ErrorCode.COMMENT_NOT_FOUND, "Comment not found: " + commentId));
|
ErrorCode.COMMENT_NOT_FOUND, "Comment not found: " + commentId));
|
||||||
@@ -61,8 +70,11 @@ public class CommentService {
|
|||||||
.authorId(author.getId())
|
.authorId(author.getId())
|
||||||
.authorName(resolveAuthorName(author))
|
.authorName(resolveAuthorName(author))
|
||||||
.build();
|
.build();
|
||||||
|
saveMentions(reply, mentionedUserIds);
|
||||||
DocumentComment saved = commentRepository.save(reply);
|
DocumentComment saved = commentRepository.save(reply);
|
||||||
|
withMentionDTOs(saved);
|
||||||
notificationService.notifyReply(saved, root);
|
notificationService.notifyReply(saved, root);
|
||||||
|
notificationService.notifyMentions(mentionedUserIds, saved);
|
||||||
return saved;
|
return saved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +101,29 @@ public class CommentService {
|
|||||||
|
|
||||||
// ─── private helpers ──────────────────────────────────────────────────────
|
// ─── private helpers ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
private List<DocumentComment> withReplies(List<DocumentComment> roots) {
|
private List<DocumentComment> withRepliesAndMentions(List<DocumentComment> roots) {
|
||||||
roots.forEach(root -> root.setReplies(commentRepository.findByParentId(root.getId())));
|
roots.forEach(root -> {
|
||||||
|
List<DocumentComment> replies = commentRepository.findByParentId(root.getId());
|
||||||
|
replies.forEach(this::withMentionDTOs);
|
||||||
|
root.setReplies(replies);
|
||||||
|
withMentionDTOs(root);
|
||||||
|
});
|
||||||
return roots;
|
return roots;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void saveMentions(DocumentComment comment, List<UUID> mentionedUserIds) {
|
||||||
|
if (mentionedUserIds == null || mentionedUserIds.isEmpty()) return;
|
||||||
|
List<AppUser> users = userRepository.findAllById(mentionedUserIds);
|
||||||
|
comment.setMentions(users);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void withMentionDTOs(DocumentComment comment) {
|
||||||
|
List<MentionDTO> dtos = comment.getMentions().stream()
|
||||||
|
.map(u -> new MentionDTO(u.getId(), u.getFirstName(), u.getLastName()))
|
||||||
|
.toList();
|
||||||
|
comment.setMentionDTOs(dtos);
|
||||||
|
}
|
||||||
|
|
||||||
private DocumentComment findComment(UUID documentId, UUID commentId) {
|
private DocumentComment findComment(UUID documentId, UUID commentId) {
|
||||||
return commentRepository.findById(commentId)
|
return commentRepository.findById(commentId)
|
||||||
.filter(c -> documentId.equals(c.getDocumentId()))
|
.filter(c -> documentId.equals(c.getDocumentId()))
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package org.raddatz.familienarchiv.service;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.raddatz.familienarchiv.model.AppUser;
|
||||||
|
import org.raddatz.familienarchiv.repository.AppUserRepository;
|
||||||
|
import org.springframework.data.domain.PageRequest;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class UserSearchService {
|
||||||
|
|
||||||
|
private static final int MAX_RESULTS = 10;
|
||||||
|
|
||||||
|
private final AppUserRepository userRepository;
|
||||||
|
|
||||||
|
public List<AppUser> search(String query) {
|
||||||
|
if (query == null || query.isBlank()) return List.of();
|
||||||
|
return userRepository.searchByNameOrUsername(query.trim(), PageRequest.of(0, MAX_RESULTS));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
CREATE TABLE comment_mentions (
|
||||||
|
comment_id UUID NOT NULL REFERENCES document_comments(id) ON DELETE CASCADE,
|
||||||
|
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||||
|
PRIMARY KEY (comment_id, user_id)
|
||||||
|
);
|
||||||
@@ -81,7 +81,7 @@ class CommentControllerTest {
|
|||||||
void postDocumentComment_returns201_whenHasPermission() throws Exception {
|
void postDocumentComment_returns201_whenHasPermission() throws Exception {
|
||||||
DocumentComment saved = DocumentComment.builder()
|
DocumentComment saved = DocumentComment.builder()
|
||||||
.id(COMMENT_ID).documentId(DOC_ID).authorName("Hans").content("Test comment").build();
|
.id(COMMENT_ID).documentId(DOC_ID).authorName("Hans").content("Test comment").build();
|
||||||
when(commentService.postComment(any(), any(), any(), any())).thenReturn(saved);
|
when(commentService.postComment(any(), any(), any(), any(), any())).thenReturn(saved);
|
||||||
|
|
||||||
mockMvc.perform(post("/api/documents/" + DOC_ID + "/comments")
|
mockMvc.perform(post("/api/documents/" + DOC_ID + "/comments")
|
||||||
.contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON))
|
.contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON))
|
||||||
@@ -104,7 +104,7 @@ class CommentControllerTest {
|
|||||||
DocumentComment saved = DocumentComment.builder()
|
DocumentComment saved = DocumentComment.builder()
|
||||||
.id(UUID.randomUUID()).documentId(DOC_ID).parentId(COMMENT_ID)
|
.id(UUID.randomUUID()).documentId(DOC_ID).parentId(COMMENT_ID)
|
||||||
.authorName("Anna").content("Test comment").build();
|
.authorName("Anna").content("Test comment").build();
|
||||||
when(commentService.replyToComment(any(), any(), any(), any())).thenReturn(saved);
|
when(commentService.replyToComment(any(), any(), any(), any(), any())).thenReturn(saved);
|
||||||
|
|
||||||
mockMvc.perform(post("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID + "/replies")
|
mockMvc.perform(post("/api/documents/" + DOC_ID + "/comments/" + COMMENT_ID + "/replies")
|
||||||
.contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON))
|
.contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON))
|
||||||
@@ -179,7 +179,7 @@ class CommentControllerTest {
|
|||||||
DocumentComment saved = DocumentComment.builder()
|
DocumentComment saved = DocumentComment.builder()
|
||||||
.id(UUID.randomUUID()).documentId(DOC_ID).annotationId(ANN_ID)
|
.id(UUID.randomUUID()).documentId(DOC_ID).annotationId(ANN_ID)
|
||||||
.authorName("Hans").content("Test comment").build();
|
.authorName("Hans").content("Test comment").build();
|
||||||
when(commentService.postComment(any(), any(), any(), any())).thenReturn(saved);
|
when(commentService.postComment(any(), any(), any(), any(), any())).thenReturn(saved);
|
||||||
|
|
||||||
mockMvc.perform(post("/api/documents/" + DOC_ID + "/annotations/" + ANN_ID + "/comments")
|
mockMvc.perform(post("/api/documents/" + DOC_ID + "/annotations/" + ANN_ID + "/comments")
|
||||||
.contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON))
|
.contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON))
|
||||||
@@ -194,7 +194,7 @@ class CommentControllerTest {
|
|||||||
DocumentComment saved = DocumentComment.builder()
|
DocumentComment saved = DocumentComment.builder()
|
||||||
.id(UUID.randomUUID()).documentId(DOC_ID).annotationId(ANN_ID)
|
.id(UUID.randomUUID()).documentId(DOC_ID).annotationId(ANN_ID)
|
||||||
.parentId(COMMENT_ID).authorName("Anna").content("Test comment").build();
|
.parentId(COMMENT_ID).authorName("Anna").content("Test comment").build();
|
||||||
when(commentService.replyToComment(any(), any(), any(), any())).thenReturn(saved);
|
when(commentService.replyToComment(any(), any(), any(), any(), any())).thenReturn(saved);
|
||||||
|
|
||||||
mockMvc.perform(post("/api/documents/" + DOC_ID + "/annotations/" + ANN_ID + "/comments/" + COMMENT_ID + "/replies")
|
mockMvc.perform(post("/api/documents/" + DOC_ID + "/annotations/" + ANN_ID + "/comments/" + COMMENT_ID + "/replies")
|
||||||
.contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON))
|
.contentType(MediaType.APPLICATION_JSON).content(COMMENT_JSON))
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package org.raddatz.familienarchiv.controller;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.raddatz.familienarchiv.config.SecurityConfig;
|
||||||
|
import org.raddatz.familienarchiv.model.AppUser;
|
||||||
|
import org.raddatz.familienarchiv.security.PermissionAspect;
|
||||||
|
import org.raddatz.familienarchiv.service.CustomUserDetailsService;
|
||||||
|
import org.raddatz.familienarchiv.service.UserSearchService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.aop.AopAutoConfiguration;
|
||||||
|
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.security.test.context.support.WithMockUser;
|
||||||
|
import org.springframework.test.context.bean.override.mockito.MockitoBean;
|
||||||
|
import org.springframework.test.web.servlet.MockMvc;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import static org.mockito.ArgumentMatchers.anyString;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||||
|
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||||
|
|
||||||
|
@WebMvcTest(UserSearchController.class)
|
||||||
|
@Import({SecurityConfig.class, PermissionAspect.class, AopAutoConfiguration.class})
|
||||||
|
class UserSearchControllerTest {
|
||||||
|
|
||||||
|
@Autowired MockMvc mockMvc;
|
||||||
|
|
||||||
|
@MockitoBean UserSearchService userSearchService;
|
||||||
|
@MockitoBean CustomUserDetailsService customUserDetailsService;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void search_returns401_whenUnauthenticated() throws Exception {
|
||||||
|
mockMvc.perform(get("/api/users/search").param("q", "Hans"))
|
||||||
|
.andExpect(status().isUnauthorized());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser
|
||||||
|
void search_returns200_whenAuthenticated() throws Exception {
|
||||||
|
AppUser user = AppUser.builder().id(UUID.randomUUID())
|
||||||
|
.firstName("Hans").lastName("Mueller").username("hans").build();
|
||||||
|
when(userSearchService.search("Hans")).thenReturn(List.of(user));
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/users/search").param("q", "Hans"))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$[0].firstName").value("Hans"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser
|
||||||
|
void search_returnsEmptyList_whenQueryIsEmpty() throws Exception {
|
||||||
|
when(userSearchService.search("")).thenReturn(List.of());
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/users/search").param("q", ""))
|
||||||
|
.andExpect(status().isOk())
|
||||||
|
.andExpect(jsonPath("$").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@WithMockUser
|
||||||
|
void search_returnsAtMostTenResults() throws Exception {
|
||||||
|
when(userSearchService.search(anyString())).thenReturn(List.of());
|
||||||
|
|
||||||
|
mockMvc.perform(get("/api/users/search").param("q", "a"))
|
||||||
|
.andExpect(status().isOk());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import org.raddatz.familienarchiv.exception.DomainException;
|
|||||||
import org.raddatz.familienarchiv.model.AppUser;
|
import org.raddatz.familienarchiv.model.AppUser;
|
||||||
import org.raddatz.familienarchiv.model.DocumentComment;
|
import org.raddatz.familienarchiv.model.DocumentComment;
|
||||||
import org.raddatz.familienarchiv.model.UserGroup;
|
import org.raddatz.familienarchiv.model.UserGroup;
|
||||||
|
import org.raddatz.familienarchiv.repository.AppUserRepository;
|
||||||
import org.raddatz.familienarchiv.repository.CommentRepository;
|
import org.raddatz.familienarchiv.repository.CommentRepository;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
@@ -31,6 +32,7 @@ import static org.springframework.http.HttpStatus.NOT_FOUND;
|
|||||||
class CommentServiceTest {
|
class CommentServiceTest {
|
||||||
|
|
||||||
@Mock CommentRepository commentRepository;
|
@Mock CommentRepository commentRepository;
|
||||||
|
@Mock AppUserRepository userRepository;
|
||||||
@Mock NotificationService notificationService;
|
@Mock NotificationService notificationService;
|
||||||
@InjectMocks CommentService commentService;
|
@InjectMocks CommentService commentService;
|
||||||
|
|
||||||
@@ -45,7 +47,7 @@ class CommentServiceTest {
|
|||||||
.id(UUID.randomUUID()).documentId(docId).authorName("Hans Müller").content("Test").build();
|
.id(UUID.randomUUID()).documentId(docId).authorName("Hans Müller").content("Test").build();
|
||||||
when(commentRepository.save(any())).thenReturn(saved);
|
when(commentRepository.save(any())).thenReturn(saved);
|
||||||
|
|
||||||
DocumentComment result = commentService.postComment(docId, null, "Test", author);
|
DocumentComment result = commentService.postComment(docId, null, "Test", List.of(), author);
|
||||||
|
|
||||||
assertThat(result.getAuthorName()).isEqualTo("Hans Müller");
|
assertThat(result.getAuthorName()).isEqualTo("Hans Müller");
|
||||||
}
|
}
|
||||||
@@ -58,7 +60,7 @@ class CommentServiceTest {
|
|||||||
.id(UUID.randomUUID()).documentId(docId).authorName("hans42").content("Test").build();
|
.id(UUID.randomUUID()).documentId(docId).authorName("hans42").content("Test").build();
|
||||||
when(commentRepository.save(any())).thenReturn(saved);
|
when(commentRepository.save(any())).thenReturn(saved);
|
||||||
|
|
||||||
DocumentComment result = commentService.postComment(docId, null, "Test", author);
|
DocumentComment result = commentService.postComment(docId, null, "Test", List.of(), author);
|
||||||
|
|
||||||
assertThat(result.getAuthorName()).isEqualTo("hans42");
|
assertThat(result.getAuthorName()).isEqualTo("hans42");
|
||||||
}
|
}
|
||||||
@@ -72,7 +74,7 @@ class CommentServiceTest {
|
|||||||
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("anna").build();
|
AppUser author = AppUser.builder().id(UUID.randomUUID()).username("anna").build();
|
||||||
when(commentRepository.findById(commentId)).thenReturn(Optional.empty());
|
when(commentRepository.findById(commentId)).thenReturn(Optional.empty());
|
||||||
|
|
||||||
assertThatThrownBy(() -> commentService.replyToComment(docId, commentId, "Reply", author))
|
assertThatThrownBy(() -> commentService.replyToComment(docId, commentId, "Reply", List.of(), author))
|
||||||
.isInstanceOf(DomainException.class)
|
.isInstanceOf(DomainException.class)
|
||||||
.satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(NOT_FOUND));
|
.satisfies(e -> assertThat(((DomainException) e).getStatus()).isEqualTo(NOT_FOUND));
|
||||||
|
|
||||||
@@ -97,7 +99,7 @@ class CommentServiceTest {
|
|||||||
.id(UUID.randomUUID()).documentId(docId).parentId(rootId).content("Reply2").authorName("anna").build();
|
.id(UUID.randomUUID()).documentId(docId).parentId(rootId).content("Reply2").authorName("anna").build();
|
||||||
when(commentRepository.save(any())).thenReturn(saved);
|
when(commentRepository.save(any())).thenReturn(saved);
|
||||||
|
|
||||||
DocumentComment result = commentService.replyToComment(docId, replyId, "Reply2", author);
|
DocumentComment result = commentService.replyToComment(docId, replyId, "Reply2", List.of(), author);
|
||||||
|
|
||||||
assertThat(result.getParentId()).isEqualTo(rootId);
|
assertThat(result.getParentId()).isEqualTo(rootId);
|
||||||
}
|
}
|
||||||
@@ -116,7 +118,7 @@ class CommentServiceTest {
|
|||||||
.id(UUID.randomUUID()).documentId(docId).parentId(rootId).content("Reply").authorName("anna").build();
|
.id(UUID.randomUUID()).documentId(docId).parentId(rootId).content("Reply").authorName("anna").build();
|
||||||
when(commentRepository.save(any())).thenReturn(saved);
|
when(commentRepository.save(any())).thenReturn(saved);
|
||||||
|
|
||||||
DocumentComment result = commentService.replyToComment(docId, rootId, "Reply", author);
|
DocumentComment result = commentService.replyToComment(docId, rootId, "Reply", List.of(), author);
|
||||||
|
|
||||||
assertThat(result.getParentId()).isEqualTo(rootId);
|
assertThat(result.getParentId()).isEqualTo(rootId);
|
||||||
}
|
}
|
||||||
@@ -135,7 +137,7 @@ class CommentServiceTest {
|
|||||||
when(commentRepository.findById(rootId)).thenReturn(Optional.of(root));
|
when(commentRepository.findById(rootId)).thenReturn(Optional.of(root));
|
||||||
when(commentRepository.save(any())).thenReturn(saved);
|
when(commentRepository.save(any())).thenReturn(saved);
|
||||||
|
|
||||||
commentService.replyToComment(docId, rootId, "Reply", author);
|
commentService.replyToComment(docId, rootId, "Reply", List.of(), author);
|
||||||
|
|
||||||
verify(notificationService).notifyReply(eq(saved), eq(root));
|
verify(notificationService).notifyReply(eq(saved), eq(root));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user