feat(#145): add type and read filter params to GET /api/notifications
Dashboard widget uses ?type=MENTION&read=false to fetch unread mentions. Also adds MethodArgumentTypeMismatchException → 400 handler so invalid enum values in any @RequestParam return 400 instead of 500. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -62,7 +62,7 @@ class NotificationControllerTest {
|
||||
void getNotifications_returns200_whenAuthenticatedWithNoPermissions() throws Exception {
|
||||
AppUser user = AppUser.builder().id(USER_ID).username("testuser").build();
|
||||
when(userService.findByUsername("testuser")).thenReturn(user);
|
||||
when(notificationService.getNotifications(eq(USER_ID), any()))
|
||||
when(notificationService.getNotifications(eq(USER_ID), any(), any(), any()))
|
||||
.thenReturn(new PageImpl<>(List.of()));
|
||||
|
||||
mockMvc.perform(get("/api/notifications"))
|
||||
@@ -78,7 +78,7 @@ class NotificationControllerTest {
|
||||
UUID.randomUUID(), null, false, LocalDateTime.now(), "Anna Smith");
|
||||
|
||||
when(userService.findByUsername("testuser")).thenReturn(user);
|
||||
when(notificationService.getNotifications(eq(USER_ID), any()))
|
||||
when(notificationService.getNotifications(eq(USER_ID), any(), any(), any()))
|
||||
.thenReturn(new PageImpl<>(List.of(dto), PageRequest.of(0, 10), 1));
|
||||
|
||||
mockMvc.perform(get("/api/notifications"))
|
||||
@@ -91,13 +91,36 @@ class NotificationControllerTest {
|
||||
void getNotifications_returnsOnlyCurrentUsersNotifications() throws Exception {
|
||||
AppUser user = AppUser.builder().id(USER_ID).username("testuser").build();
|
||||
when(userService.findByUsername("testuser")).thenReturn(user);
|
||||
when(notificationService.getNotifications(eq(USER_ID), any()))
|
||||
when(notificationService.getNotifications(eq(USER_ID), any(), any(), any()))
|
||||
.thenReturn(new PageImpl<>(List.of()));
|
||||
|
||||
mockMvc.perform(get("/api/notifications"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
verify(notificationService).getNotifications(eq(USER_ID), any());
|
||||
verify(notificationService).getNotifications(eq(USER_ID), any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "testuser", authorities = {"READ_ALL"})
|
||||
void getNotifications_withTypeAndReadFalse_passesFiltersToService() throws Exception {
|
||||
AppUser user = AppUser.builder().id(USER_ID).username("testuser").build();
|
||||
when(userService.findByUsername("testuser")).thenReturn(user);
|
||||
when(notificationService.getNotifications(eq(USER_ID), eq(NotificationType.MENTION), eq(false), any()))
|
||||
.thenReturn(new PageImpl<>(List.of()));
|
||||
|
||||
mockMvc.perform(get("/api/notifications")
|
||||
.param("type", "MENTION")
|
||||
.param("read", "false"))
|
||||
.andExpect(status().isOk());
|
||||
|
||||
verify(notificationService).getNotifications(eq(USER_ID), eq(NotificationType.MENTION), eq(false), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@WithMockUser(username = "testuser", authorities = {"READ_ALL"})
|
||||
void getNotifications_withInvalidType_returns400() throws Exception {
|
||||
mockMvc.perform(get("/api/notifications").param("type", "INVALID_TYPE"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
// ─── POST /api/notifications/read-all ────────────────────────────────────
|
||||
@@ -199,7 +222,7 @@ class NotificationControllerTest {
|
||||
void getNotifications_returns200_whenUserHasOnlyWriteAll() throws Exception {
|
||||
AppUser user = AppUser.builder().id(USER_ID).username("testuser").build();
|
||||
when(userService.findByUsername("testuser")).thenReturn(user);
|
||||
when(notificationService.getNotifications(eq(USER_ID), any()))
|
||||
when(notificationService.getNotifications(eq(USER_ID), any(), any(), any()))
|
||||
.thenReturn(new PageImpl<>(List.of()));
|
||||
|
||||
mockMvc.perform(get("/api/notifications"))
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package org.raddatz.familienarchiv.repository;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.raddatz.familienarchiv.PostgresContainerConfig;
|
||||
import org.raddatz.familienarchiv.config.FlywayConfig;
|
||||
import org.raddatz.familienarchiv.model.AppUser;
|
||||
import org.raddatz.familienarchiv.model.Notification;
|
||||
import org.raddatz.familienarchiv.model.NotificationType;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@DataJpaTest
|
||||
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
|
||||
@Import({PostgresContainerConfig.class, FlywayConfig.class})
|
||||
class NotificationRepositoryTest {
|
||||
|
||||
@Autowired NotificationRepository notificationRepository;
|
||||
@Autowired AppUserRepository appUserRepository;
|
||||
|
||||
private AppUser userA;
|
||||
private AppUser userB;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
notificationRepository.deleteAll();
|
||||
appUserRepository.deleteAll();
|
||||
userA = appUserRepository.save(AppUser.builder().username("userA").password("pw").build());
|
||||
userB = appUserRepository.save(AppUser.builder().username("userB").password("pw").build());
|
||||
}
|
||||
|
||||
// ─── findByRecipientIdAndTypeAndReadFalse ─────────────────────────────────
|
||||
|
||||
@Test
|
||||
void returnsOnlyUnreadMentions_forTargetUser() {
|
||||
notificationRepository.save(mention(userA, false)); // ✓ match
|
||||
notificationRepository.save(mention(userA, true)); // read — excluded
|
||||
notificationRepository.save(reply(userA, false)); // REPLY — excluded
|
||||
notificationRepository.save(mention(userB, false)); // different user — excluded
|
||||
|
||||
Page<Notification> result = notificationRepository
|
||||
.findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(
|
||||
userA.getId(), NotificationType.MENTION, Pageable.ofSize(10));
|
||||
|
||||
assertThat(result.getContent()).hasSize(1);
|
||||
assertThat(result.getContent().get(0).getRecipient().getId()).isEqualTo(userA.getId());
|
||||
assertThat(result.getContent().get(0).getType()).isEqualTo(NotificationType.MENTION);
|
||||
assertThat(result.getContent().get(0).isRead()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnsEmpty_whenAllMentionsAreRead() {
|
||||
notificationRepository.save(mention(userA, true));
|
||||
|
||||
Page<Notification> result = notificationRepository
|
||||
.findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(
|
||||
userA.getId(), NotificationType.MENTION, Pageable.ofSize(10));
|
||||
|
||||
assertThat(result.getContent()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
void respectsSizeLimit() {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
notificationRepository.save(mention(userA, false));
|
||||
}
|
||||
|
||||
Page<Notification> result = notificationRepository
|
||||
.findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(
|
||||
userA.getId(), NotificationType.MENTION, Pageable.ofSize(3));
|
||||
|
||||
assertThat(result.getContent()).hasSize(3);
|
||||
assertThat(result.getTotalElements()).isEqualTo(5);
|
||||
}
|
||||
|
||||
// ─── helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
private Notification mention(AppUser recipient, boolean read) {
|
||||
return Notification.builder()
|
||||
.recipient(recipient)
|
||||
.type(NotificationType.MENTION)
|
||||
.actorName("Tester")
|
||||
.read(read)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Notification reply(AppUser recipient, boolean read) {
|
||||
return Notification.builder()
|
||||
.recipient(recipient)
|
||||
.type(NotificationType.REPLY)
|
||||
.actorName("Tester")
|
||||
.read(read)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,9 @@ import org.springframework.mail.MailSendException;
|
||||
import org.springframework.mail.SimpleMailMessage;
|
||||
import org.springframework.mail.javamail.JavaMailSender;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
@@ -356,6 +359,33 @@ class NotificationServiceTest {
|
||||
assertThat(captor.getValue().getText()).contains("annotationId=" + annotationId);
|
||||
}
|
||||
|
||||
// ─── getNotifications — filter dispatch ──────────────────────────────────
|
||||
|
||||
@Test
|
||||
void getNotifications_withNoFilters_usesUnfilteredRepoMethod() {
|
||||
when(notificationRepository.findByRecipientIdOrderByCreatedAtDesc(eq(userA.getId()), any()))
|
||||
.thenReturn(Page.empty());
|
||||
|
||||
notificationService.getNotifications(userA.getId(), null, null, Pageable.ofSize(10));
|
||||
|
||||
verify(notificationRepository).findByRecipientIdOrderByCreatedAtDesc(eq(userA.getId()), any());
|
||||
verify(notificationRepository, never())
|
||||
.findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getNotifications_withTypeAndReadFalse_usesFilteredRepoMethod() {
|
||||
when(notificationRepository.findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(
|
||||
eq(userA.getId()), eq(NotificationType.MENTION), any()))
|
||||
.thenReturn(Page.empty());
|
||||
|
||||
notificationService.getNotifications(userA.getId(), NotificationType.MENTION, false, Pageable.ofSize(3));
|
||||
|
||||
verify(notificationRepository).findByRecipientIdAndTypeAndReadFalseOrderByCreatedAtDesc(
|
||||
eq(userA.getId()), eq(NotificationType.MENTION), any());
|
||||
verify(notificationRepository, never()).findByRecipientIdOrderByCreatedAtDesc(any(), any());
|
||||
}
|
||||
|
||||
// ─── private helpers ──────────────────────────────────────────────────────
|
||||
|
||||
private DocumentComment commentWithAuthor(UUID id, UUID parentId, UUID authorId, String authorName) {
|
||||
|
||||
Reference in New Issue
Block a user