diff --git a/server/src/main/java/br/com/tasknoteapp/server/repository/TagRepository.java b/server/src/main/java/br/com/tasknoteapp/server/repository/TagRepository.java
index 187ffa5..a999d41 100644
--- a/server/src/main/java/br/com/tasknoteapp/server/repository/TagRepository.java
+++ b/server/src/main/java/br/com/tasknoteapp/server/repository/TagRepository.java
@@ -24,4 +24,8 @@ and not exists (select 1 from TaskEntity tk where t member of tk.tags)
and not exists (select 1 from NoteEntity n where t member of n.tags)
""")
void deleteOrphanedTags(@Param("userId") Long userId);
+
+ @Modifying(flushAutomatically = true, clearAutomatically = true)
+ @Query("delete from TagEntity t where t.user.id = :userId")
+ void deleteAllForUser(@Param("userId") Long userId);
}
diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java b/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java
index ba4416e..0feaa4b 100644
--- a/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java
+++ b/server/src/main/java/br/com/tasknoteapp/server/service/NoteService.java
@@ -196,6 +196,29 @@ public NoteResponse patchNote(Long noteId, NotePatchRequest patch) {
return NoteResponse.fromEntity(patchedNote, getNoteUrl(patchedNote.getId()));
}
+ /**
+ * Delete all notes and their URLs for the current user, regardless of archived state.
+ *
+ *
This is intended for account deletion, where the trash-can rule (only archived notes may be
+ * deleted) does not apply.
+ */
+ @Transactional
+ public void deleteAllNotesForCurrentUser() {
+ UserEntity user = getCurrentUser();
+
+ logger.info("Deleting all notes for user ID {}", user.getId());
+
+ List notes = noteRepository.findAllByUser_id(user.getId());
+ for (NoteEntity note : notes) {
+ noteUrlRepository.deleteByNote_id(note.getId());
+ noteRepository.delete(note);
+ }
+
+ tagRepository.deleteOrphanedTags(user.getId());
+
+ logger.info("All {} notes deleted for user ID {}", notes.size(), user.getId());
+ }
+
/**
* Delete a note and all its URLs, if any, for the user.
*
diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/UserSessionService.java b/server/src/main/java/br/com/tasknoteapp/server/service/UserSessionService.java
index 6172550..22ca5d1 100644
--- a/server/src/main/java/br/com/tasknoteapp/server/service/UserSessionService.java
+++ b/server/src/main/java/br/com/tasknoteapp/server/service/UserSessionService.java
@@ -2,8 +2,8 @@
import br.com.tasknoteapp.server.entity.UserEntity;
import br.com.tasknoteapp.server.exception.UserNotFoundException;
+import br.com.tasknoteapp.server.repository.TagRepository;
import br.com.tasknoteapp.server.response.JwtAuthenticationResponse;
-import br.com.tasknoteapp.server.response.NoteResponse;
import br.com.tasknoteapp.server.response.TaskResponse;
import br.com.tasknoteapp.server.response.UserResponse;
import br.com.tasknoteapp.server.util.SecurityUtil;
@@ -26,18 +26,25 @@ public class UserSessionService {
private final NoteService noteService;
+ private final TagRepository tagRepository;
+
/**
* Constructor for UserSessionService.
*
* @param authService the authentication service
* @param taskService the task service
* @param noteService the note service
+ * @param tagRepository the tag repository
*/
public UserSessionService(
- AuthService authService, TaskService taskService, NoteService noteService) {
+ AuthService authService,
+ TaskService taskService,
+ NoteService noteService,
+ TagRepository tagRepository) {
this.authService = authService;
this.taskService = taskService;
this.noteService = noteService;
+ this.tagRepository = tagRepository;
}
/**
@@ -75,13 +82,9 @@ public UserResponse deleteCurrentUserAccount() {
}
}
- List notes = noteService.getAllNotes();
- for (NoteResponse note : notes) {
- Long noteId = note != null ? note.id() : null;
- if (noteId != null) {
- noteService.deleteNote(noteId);
- }
- }
+ noteService.deleteAllNotesForCurrentUser();
+
+ tagRepository.deleteAllForUser(userOptional.get().getId());
UserResponse response = authService.deleteUserAccount();
logger.info("User account deleted for user ID {}", userOptional.get().getId());
diff --git a/server/src/test/java/br/com/tasknoteapp/server/service/AccountDeletionIntTest.java b/server/src/test/java/br/com/tasknoteapp/server/service/AccountDeletionIntTest.java
new file mode 100644
index 0000000..5910270
--- /dev/null
+++ b/server/src/test/java/br/com/tasknoteapp/server/service/AccountDeletionIntTest.java
@@ -0,0 +1,155 @@
+package br.com.tasknoteapp.server.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import br.com.tasknoteapp.server.entity.NoteEntity;
+import br.com.tasknoteapp.server.entity.NoteUrlEntity;
+import br.com.tasknoteapp.server.entity.TagEntity;
+import br.com.tasknoteapp.server.entity.TaskEntity;
+import br.com.tasknoteapp.server.entity.UserEntity;
+import br.com.tasknoteapp.server.exception.NoteArchivedException;
+import br.com.tasknoteapp.server.repository.NoteRepository;
+import br.com.tasknoteapp.server.repository.NoteUrlRepository;
+import br.com.tasknoteapp.server.repository.TagRepository;
+import br.com.tasknoteapp.server.repository.TaskRepository;
+import br.com.tasknoteapp.server.repository.UserRepository;
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Set;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.transaction.annotation.Transactional;
+
+@SpringBootTest
+@Transactional
+class AccountDeletionIntTest {
+
+ @Autowired private UserSessionService userSessionService;
+
+ @Autowired private NoteService noteService;
+
+ @Autowired private UserRepository userRepository;
+
+ @Autowired private NoteRepository noteRepository;
+
+ @Autowired private NoteUrlRepository noteUrlRepository;
+
+ @Autowired private TaskRepository taskRepository;
+
+ @Autowired private TagRepository tagRepository;
+
+ private UserEntity user;
+
+ @BeforeEach
+ void setUp() {
+ user = new UserEntity();
+ user.setEmail("account-deletion@domain.com");
+ user.setPassword("a1b2c3d4f5g6");
+ user.setAdmin(false);
+ user.setCreatedAt(LocalDateTime.now());
+ user.setLastPasswordChange(LocalDateTime.now());
+ user = userRepository.save(user);
+
+ SecurityContext context = SecurityContextHolder.createEmptyContext();
+ context.setAuthentication(
+ new UsernamePasswordAuthenticationToken(user.getEmail(), null, List.of()));
+ SecurityContextHolder.setContext(context);
+ }
+
+ @AfterEach
+ void tearDown() {
+ SecurityContextHolder.clearContext();
+ }
+
+ @Test
+ @DisplayName("Delete account with mixed archived and non-archived notes should remove all data")
+ void deleteAccount_mixedArchivedNotes_shouldRemoveAllUserData() {
+ TagEntity tag = tagRepository.save(new TagEntity("work", user));
+
+ NoteEntity activeNote = new NoteEntity();
+ activeNote.setTitle("Active note");
+ activeNote.setDescription("Not archived");
+ activeNote.setUser(user);
+ activeNote.setLastUpdate(LocalDateTime.now());
+ activeNote.setTags(Set.of(tag));
+ activeNote = noteRepository.save(activeNote);
+
+ NoteUrlEntity noteUrl = new NoteUrlEntity();
+ noteUrl.setUrl("http://example.com");
+ noteUrl.setNote(activeNote);
+ noteUrlRepository.save(noteUrl);
+
+ NoteEntity archivedNote = new NoteEntity();
+ archivedNote.setTitle("Archived note");
+ archivedNote.setDescription("Archived");
+ archivedNote.setUser(user);
+ archivedNote.setLastUpdate(LocalDateTime.now());
+ archivedNote.setArchived(true);
+ archivedNote.setTags(Set.of(tag));
+ archivedNote = noteRepository.save(archivedNote);
+
+ TaskEntity task = new TaskEntity();
+ task.setDescription("A task");
+ task.setCompleted(false);
+ task.setUser(user);
+ task.setLastUpdate(LocalDateTime.now());
+ task.setTags(Set.of(tag));
+ taskRepository.save(task);
+
+ Long userId = user.getId();
+ final Long activeNoteId = activeNote.getId();
+
+ userSessionService.deleteCurrentUserAccount();
+
+ assertTrue(userRepository.findById(userId).isEmpty());
+ assertTrue(noteRepository.findAllByUser_id(userId).isEmpty());
+ assertTrue(noteUrlRepository.findByNote_id(activeNoteId).isEmpty());
+ assertTrue(taskRepository.findAllByUser_id(userId).isEmpty());
+ assertTrue(tagRepository.findAllByUser_idOrderByNameAsc(userId).isEmpty());
+ }
+
+ @Test
+ @DisplayName("Single-note delete should still reject non-archived notes")
+ void deleteNote_nonArchived_shouldStillThrow() {
+ NoteEntity activeNote = new NoteEntity();
+ activeNote.setTitle("Active note");
+ activeNote.setDescription("Not archived");
+ activeNote.setUser(user);
+ activeNote.setLastUpdate(LocalDateTime.now());
+ activeNote = noteRepository.save(activeNote);
+
+ Long noteId = activeNote.getId();
+
+ assertThrows(NoteArchivedException.class, () -> noteService.deleteNote(noteId));
+
+ assertTrue(noteRepository.findById(noteId).isPresent());
+ }
+
+ @Test
+ @DisplayName("Single-note delete of archived note should still work")
+ void deleteNote_archived_shouldSucceed() {
+ NoteEntity archivedNote = new NoteEntity();
+ archivedNote.setTitle("Archived note");
+ archivedNote.setDescription("Archived");
+ archivedNote.setUser(user);
+ archivedNote.setLastUpdate(LocalDateTime.now());
+ archivedNote.setArchived(true);
+ archivedNote = noteRepository.save(archivedNote);
+
+ Long noteId = archivedNote.getId();
+
+ noteService.deleteNote(noteId);
+
+ assertTrue(noteRepository.findById(noteId).isEmpty());
+ assertEquals(0, noteRepository.findAllByUser_id(user.getId()).size());
+ }
+}
diff --git a/server/src/test/java/br/com/tasknoteapp/server/service/NoteServiceTest.java b/server/src/test/java/br/com/tasknoteapp/server/service/NoteServiceTest.java
index 3694392..5d3c549 100644
--- a/server/src/test/java/br/com/tasknoteapp/server/service/NoteServiceTest.java
+++ b/server/src/test/java/br/com/tasknoteapp/server/service/NoteServiceTest.java
@@ -162,6 +162,28 @@ void deleteNote() {
verify(noteRepository, times(1)).delete(note);
}
+ @Test
+ void deleteAllNotesForCurrentUser() {
+ NoteEntity archivedNote = new NoteEntity();
+ archivedNote.setId(2L);
+ archivedNote.setTitle("Archived Note");
+ archivedNote.setDescription("Archived Description");
+ archivedNote.setUser(user);
+ archivedNote.setArchived(true);
+
+ when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail()));
+ when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user));
+ when(noteRepository.findAllByUser_id(user.getId())).thenReturn(List.of(note, archivedNote));
+
+ noteService.deleteAllNotesForCurrentUser();
+
+ verify(noteUrlRepository, times(1)).deleteByNote_id(note.getId());
+ verify(noteUrlRepository, times(1)).deleteByNote_id(archivedNote.getId());
+ verify(noteRepository, times(1)).delete(note);
+ verify(noteRepository, times(1)).delete(archivedNote);
+ verify(tagRepository, times(1)).deleteOrphanedTags(user.getId());
+ }
+
@Test
void searchNotes() {
when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail()));
diff --git a/server/src/test/java/br/com/tasknoteapp/server/service/UserSessionServiceTest.java b/server/src/test/java/br/com/tasknoteapp/server/service/UserSessionServiceTest.java
index ef66a7c..80b7862 100644
--- a/server/src/test/java/br/com/tasknoteapp/server/service/UserSessionServiceTest.java
+++ b/server/src/test/java/br/com/tasknoteapp/server/service/UserSessionServiceTest.java
@@ -7,7 +7,7 @@
import br.com.tasknoteapp.server.entity.UserEntity;
import br.com.tasknoteapp.server.exception.UserNotFoundException;
-import br.com.tasknoteapp.server.response.NoteResponse;
+import br.com.tasknoteapp.server.repository.TagRepository;
import br.com.tasknoteapp.server.response.TaskResponse;
import br.com.tasknoteapp.server.response.UserResponse;
import java.util.List;
@@ -26,12 +26,14 @@ class UserSessionServiceTest {
@Mock private AuthService authService;
@Mock private TaskService taskService;
@Mock private NoteService noteService;
+ @Mock private TagRepository tagRepository;
private UserSessionService userSessionService;
@BeforeEach
void setUp() {
- userSessionService = new UserSessionService(authService, taskService, noteService);
+ userSessionService =
+ new UserSessionService(authService, taskService, noteService, tagRepository);
}
@Test
@@ -43,12 +45,9 @@ void deleteCurrentUserAccount_happyPath_shouldSucceed() {
TaskResponse task =
new TaskResponse(1L, false, "Task 1", true, null, null, null, null, List.of());
- NoteResponse note =
- new NoteResponse(1L, "Note 1", "Description", null, null, null, false, null, false);
when(authService.getCurrentUser()).thenReturn(Optional.of(user));
when(taskService.getAllTasks()).thenReturn(List.of(task));
- when(noteService.getAllNotes()).thenReturn(List.of(note));
when(authService.deleteUserAccount())
.thenReturn(
new UserResponse(
@@ -61,7 +60,8 @@ void deleteCurrentUserAccount_happyPath_shouldSucceed() {
assert response.userId() == 1L;
assert response.email().equals("user@domain.com");
verify(taskService).deleteTask(task.id());
- verify(noteService).deleteNote(note.id());
+ verify(noteService).deleteAllNotesForCurrentUser();
+ verify(tagRepository).deleteAllForUser(user.getId());
verify(authService).deleteUserAccount();
}
@@ -72,6 +72,6 @@ void deleteCurrentUserAccount_userNotFound_shouldThrowException() {
// Act & Assert
assertThrows(UserNotFoundException.class, () -> userSessionService.deleteCurrentUserAccount());
- verifyNoInteractions(taskService, noteService);
+ verifyNoInteractions(taskService, noteService, tagRepository);
}
}