Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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<NoteEntity> 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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -75,13 +82,9 @@ public UserResponse deleteCurrentUserAccount() {
}
}

List<NoteResponse> 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());
Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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();
}

Expand All @@ -72,6 +72,6 @@ void deleteCurrentUserAccount_userNotFound_shouldThrowException() {

// Act & Assert
assertThrows(UserNotFoundException.class, () -> userSessionService.deleteCurrentUserAccount());
verifyNoInteractions(taskService, noteService);
verifyNoInteractions(taskService, noteService, tagRepository);
}
}