diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index f974c93..6acc985 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -39,7 +39,7 @@ services: ports: - "8585:8585" - "5005:5005" - image: maven:3.9.9-eclipse-temurin-25 + image: maven:3.9.12-eclipse-temurin-25 entrypoint: './mvnw -ntp spring-boot:run -Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005" -Dmaven.plugin.validation=VERBOSE' working_dir: /app volumes: diff --git a/server/pom.xml b/server/pom.xml index 2849a5f..682a868 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -173,7 +173,7 @@ true true - true + false diff --git a/server/src/main/java/br/com/tasknoteapp/server/config/SecurityConfig.java b/server/src/main/java/br/com/tasknoteapp/server/config/SecurityConfig.java index 8a14b1e..ba7f707 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/config/SecurityConfig.java +++ b/server/src/main/java/br/com/tasknoteapp/server/config/SecurityConfig.java @@ -14,6 +14,7 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @@ -59,6 +60,8 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .permitAll()) .httpBasic(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) + .sessionManagement( + session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling( exceptionHandling -> exceptionHandling.authenticationEntryPoint( diff --git a/server/src/main/java/br/com/tasknoteapp/server/controller/NoteController.java b/server/src/main/java/br/com/tasknoteapp/server/controller/NoteController.java index 93ed495..2dc772f 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/controller/NoteController.java +++ b/server/src/main/java/br/com/tasknoteapp/server/controller/NoteController.java @@ -1,6 +1,5 @@ package br.com.tasknoteapp.server.controller; -import br.com.tasknoteapp.server.entity.NoteEntity; import br.com.tasknoteapp.server.exception.NoteNotFoundException; import br.com.tasknoteapp.server.request.NotePatchRequest; import br.com.tasknoteapp.server.request.NoteRequest; @@ -77,8 +76,8 @@ public ResponseEntity patchNote( */ @PostMapping public ResponseEntity postNotes(@RequestBody @Valid NoteRequest noteRequest) { - NoteEntity createdNote = noteService.createNote(noteRequest); - return ResponseEntity.status(HttpStatus.CREATED).body(NoteResponse.fromEntity(createdNote)); + NoteResponse createdNote = noteService.createNote(noteRequest); + return ResponseEntity.status(HttpStatus.CREATED).body(createdNote); } /** diff --git a/server/src/main/java/br/com/tasknoteapp/server/entity/NoteEntity.java b/server/src/main/java/br/com/tasknoteapp/server/entity/NoteEntity.java index 1ea0e06..dd3d21b 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/entity/NoteEntity.java +++ b/server/src/main/java/br/com/tasknoteapp/server/entity/NoteEntity.java @@ -8,7 +8,6 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import java.time.LocalDateTime; @@ -30,9 +29,6 @@ public class NoteEntity { @ManyToOne(fetch = FetchType.LAZY) private UserEntity user; - @OneToOne(mappedBy = "note", fetch = FetchType.LAZY) - private NoteUrlEntity noteUrl; - @Column(name = "tag", nullable = true, length = 30) private String tag; @@ -77,14 +73,6 @@ public void setUser(UserEntity user) { this.user = user; } - public NoteUrlEntity getNoteUrl() { - return noteUrl; - } - - public void setNoteUrl(NoteUrlEntity noteUrl) { - this.noteUrl = noteUrl; - } - public String getTag() { return tag; } diff --git a/server/src/main/java/br/com/tasknoteapp/server/entity/UserEntity.java b/server/src/main/java/br/com/tasknoteapp/server/entity/UserEntity.java index f0841e1..c72c9b3 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/entity/UserEntity.java +++ b/server/src/main/java/br/com/tasknoteapp/server/entity/UserEntity.java @@ -5,7 +5,6 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import java.time.LocalDateTime; import java.util.Collection; @@ -41,9 +40,6 @@ public class UserEntity implements UserDetails { @Column(name = "name", length = 20) private String name; - @OneToMany(mappedBy = "user") - private List tasks; - @Column(name = "email_confirmed_at", nullable = true) private LocalDateTime emailConfirmedAt; @@ -147,14 +143,6 @@ public void setName(String name) { this.name = name; } - public List getTasks() { - return tasks; - } - - public void setTasks(List tasks) { - this.tasks = tasks; - } - public LocalDateTime getEmailConfirmedAt() { return emailConfirmedAt; } diff --git a/server/src/main/java/br/com/tasknoteapp/server/repository/NoteRepository.java b/server/src/main/java/br/com/tasknoteapp/server/repository/NoteRepository.java index e8d9606..a2fb375 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/repository/NoteRepository.java +++ b/server/src/main/java/br/com/tasknoteapp/server/repository/NoteRepository.java @@ -13,6 +13,8 @@ public interface NoteRepository extends JpaRepository { Optional findByShareToken(String shareToken); + Optional findByIdAndUser_id(Long id, Long userId); + @Query( "select n from NoteEntity n where (upper(n.title) like %?1% or upper(n.description) like" + " %?1%) and n.user.id = ?2") diff --git a/server/src/main/java/br/com/tasknoteapp/server/repository/NoteUrlRepository.java b/server/src/main/java/br/com/tasknoteapp/server/repository/NoteUrlRepository.java index 27671b7..98a5e9b 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/repository/NoteUrlRepository.java +++ b/server/src/main/java/br/com/tasknoteapp/server/repository/NoteUrlRepository.java @@ -1,10 +1,16 @@ package br.com.tasknoteapp.server.repository; import br.com.tasknoteapp.server.entity.NoteUrlEntity; +import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; /** This interface represents a note url repository, for database access. */ public interface NoteUrlRepository extends JpaRepository { + Optional findByNote_id(Long noteId); + + List findAllByNote_idIn(List noteIds); + void deleteByNote_id(Long noteId); } diff --git a/server/src/main/java/br/com/tasknoteapp/server/repository/UserPwdLimitRepository.java b/server/src/main/java/br/com/tasknoteapp/server/repository/UserPwdLimitRepository.java index bfde862..3a8a28d 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/repository/UserPwdLimitRepository.java +++ b/server/src/main/java/br/com/tasknoteapp/server/repository/UserPwdLimitRepository.java @@ -12,6 +12,8 @@ public interface UserPwdLimitRepository extends JpaRepository findAllByUser_id(Long userId, Sort sort); + List findTop3ByUser_idOrderByWhenHappenedDesc(Long userId); + @Modifying @Query("delete UserPwdLimitEntity u where u.user.id = ?1") void deleteAllForUser(Long userId); diff --git a/server/src/main/java/br/com/tasknoteapp/server/response/NoteResponse.java b/server/src/main/java/br/com/tasknoteapp/server/response/NoteResponse.java index 47cbaba..6f65ce0 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/response/NoteResponse.java +++ b/server/src/main/java/br/com/tasknoteapp/server/response/NoteResponse.java @@ -1,9 +1,7 @@ package br.com.tasknoteapp.server.response; import br.com.tasknoteapp.server.entity.NoteEntity; -import br.com.tasknoteapp.server.entity.NoteUrlEntity; import br.com.tasknoteapp.server.util.TimeAgoUtil; -import java.util.Objects; /** This record represents a task and its urls object to be returned. */ public record NoteResponse( @@ -16,9 +14,7 @@ public record NoteResponse( * @param entity The NoteEntity source data. * @return NoteResponse instance with all note data and urls, if any. */ - public static NoteResponse fromEntity(NoteEntity entity) { - NoteUrlEntity noteUrl = entity.getNoteUrl(); - String url = Objects.isNull(noteUrl) ? null : noteUrl.getUrl(); + public static NoteResponse fromEntity(NoteEntity entity, String url) { String timeAgoFmt = TimeAgoUtil.format(entity.getLastUpdate()); return new NoteResponse( diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java b/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java index c4a5751..e1e35bf 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/AuthService.java @@ -36,8 +36,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.env.Environment; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Direction; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; @@ -513,8 +511,10 @@ private Optional getGravatarImageUrl(String email) { } private void checkLoginAttemptLimit(Long userId) { - Sort sort = Sort.by(Direction.DESC, "whenHappened"); - List userPwdList = userPwdLimitRepository.findAllByUser_id(userId, sort); + // Fetch only the 3 most recent failed attempts to avoid loading unbounded rows for + // targeted/brute-forced accounts. + List userPwdList = + userPwdLimitRepository.findTop3ByUser_idOrderByWhenHappenedDesc(userId); logger.warn("login count attempt for user {}: {}", userId, userPwdList.size()); diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/MailgunEmailService.java b/server/src/main/java/br/com/tasknoteapp/server/service/MailgunEmailService.java index 1d9cc62..92b8b35 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/MailgunEmailService.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/MailgunEmailService.java @@ -8,6 +8,7 @@ import br.com.tasknoteapp.server.templates.MailgunTemplateResetPwdConfirm; import br.com.tasknoteapp.server.templates.MailgunTemplateSignUp; import java.nio.charset.StandardCharsets; +import java.time.Duration; import java.util.Base64; import java.util.Objects; import org.slf4j.Logger; @@ -53,7 +54,11 @@ public MailgunEmailService( this.senderEmail = sender; this.targetEnv = targetEnv; this.restTemplate = - templateBuilder.defaultHeader(HttpHeaders.AUTHORIZATION, basicAuth("api", apiKey)).build(); + templateBuilder + .connectTimeout(Duration.ofSeconds(5)) + .readTimeout(Duration.ofSeconds(10)) + .defaultHeader(HttpHeaders.AUTHORIZATION, basicAuth("api", apiKey)) + .build(); } /** 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 6ad00c9..941c10f 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 @@ -4,7 +4,6 @@ import br.com.tasknoteapp.server.entity.NoteUrlEntity; import br.com.tasknoteapp.server.entity.UserEntity; import br.com.tasknoteapp.server.exception.NoteNotFoundException; -import br.com.tasknoteapp.server.exception.TaskNotFoundException; import br.com.tasknoteapp.server.repository.NoteRepository; import br.com.tasknoteapp.server.repository.NoteUrlRepository; import br.com.tasknoteapp.server.request.NotePatchRequest; @@ -14,9 +13,11 @@ import jakarta.transaction.Transactional; import java.time.LocalDateTime; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.UUID; +import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; @@ -67,35 +68,39 @@ public List getAllNotes() { List notes = noteRepository.findAllByUser_id(user.getId()); logger.info(notes.size() + " notes found!"); - return notes.stream().map(NoteResponse::fromEntity).toList(); + return getNotesUrl(notes); } /** * Get a note by its id. * - * @param noteId The task id in the database. - * @return {@link NoteResponse} with the found task or throw a {@link TaskNotFoundException}. + * @param noteId The note id in the database. + * @return {@link NoteResponse} with the found note or throw a {@link NoteNotFoundException}. */ public NoteResponse getNoteById(Long noteId) { UserEntity user = getCurrentUser(); logger.info("Get note " + noteId + " to user " + user.getId()); - Optional task = noteRepository.findById(noteId); - if (task.isEmpty()) { + Optional note = noteRepository.findById(noteId); + if (note.isEmpty()) { + throw new NoteNotFoundException(); + } + + if (!note.get().getUser().getId().equals(user.getId())) { throw new NoteNotFoundException(); } logger.info("Note found! Id " + noteId); - return NoteResponse.fromEntity(task.get()); + return NoteResponse.fromEntity(note.get(), getNoteUrl(noteId)); } /** * Create a note for the user. * * @param noteRequest The note content. - * @return {@link NoteEntity} created in the database + * @return {@link NoteResponse} with created note data. */ - public NoteEntity createNote(NoteRequest noteRequest) { + public NoteResponse createNote(NoteRequest noteRequest) { UserEntity user = getCurrentUser(); logger.info("Creating note to user " + user.getId()); @@ -110,13 +115,14 @@ public NoteEntity createNote(NoteRequest noteRequest) { logger.info("Note created! Id " + created.getId()); + String savedUrl = null; if (!Objects.isNull(noteRequest.url()) && !noteRequest.url().isEmpty()) { - NoteUrlEntity urlEntity = saveUrl(note, noteRequest.url()); - note.setNoteUrl(urlEntity); + NoteUrlEntity urlEntity = saveUrl(created, noteRequest.url()); + savedUrl = urlEntity.getUrl(); } logger.info("Finished note creation!"); - return created; + return NoteResponse.fromEntity(created, savedUrl); } /** @@ -132,7 +138,7 @@ public NoteResponse patchNote(Long noteId, NotePatchRequest patch) { logger.info("Patching task " + noteId + " to user " + user.getId()); - Optional note = noteRepository.findById(noteId); + Optional note = noteRepository.findByIdAndUser_id(noteId, user.getId()); if (note.isEmpty()) { throw new NoteNotFoundException(); } @@ -154,8 +160,7 @@ public NoteResponse patchNote(Long noteId, NotePatchRequest patch) { logger.info("URL deleted from task " + noteId); if (!Objects.isNull(patch.url()) && !patch.url().isBlank()) { - NoteUrlEntity urlEntity = saveUrl(noteEntity, patch.url()); - noteEntity.setNoteUrl(urlEntity); + saveUrl(noteEntity, patch.url()); } else { logger.info("No urls to patch for task " + noteId); } @@ -165,7 +170,7 @@ public NoteResponse patchNote(Long noteId, NotePatchRequest patch) { logger.info("Note patched! Id " + patchedNote.getId()); - return NoteResponse.fromEntity(patchedNote); + return NoteResponse.fromEntity(patchedNote, getNoteUrl(patchedNote.getId())); } /** @@ -179,20 +184,15 @@ public void deleteNote(Long noteId) { logger.info("Deleting note " + noteId + " to user " + user.getId()); - Optional note = noteRepository.findById(noteId); + Optional note = noteRepository.findByIdAndUser_id(noteId, user.getId()); if (note.isEmpty()) { throw new NoteNotFoundException(); } NoteEntity noteEntity = note.get(); - NoteUrlEntity noteUrl = noteEntity.getNoteUrl(); - if (!Objects.isNull(noteUrl)) { - noteUrlRepository.delete(noteUrl); - logger.info("URL Deleted from task " + noteId); - } else { - logger.info("No urls to delete for task " + noteId); - } + noteUrlRepository.deleteByNote_id(noteId); + logger.info("URL deleted from task " + noteId); noteRepository.delete(noteEntity); @@ -213,7 +213,7 @@ public List searchNotes(String searchTerm) { List notes = noteRepository.findAllBySearchTerm(searchTerm.toUpperCase(), user.getId()); logger.info(notes.size() + " tasks found!"); - return notes.stream().map(NoteResponse::fromEntity).toList(); + return getNotesUrl(notes); } /** @@ -222,16 +222,18 @@ public List searchNotes(String searchTerm) { * @param noteId The note id from the database. * @return {@link NoteResponse} containing the updated note with share token. */ + @Transactional public NoteResponse shareNote(Long noteId) { UserEntity user = getCurrentUser(); logger.info("Sharing note " + noteId + " for user " + user.getId()); - Optional noteOpt = noteRepository.findById(noteId); + Optional noteOpt = noteRepository.findByIdAndUser_id(noteId, user.getId()); if (noteOpt.isEmpty()) { throw new NoteNotFoundException(); } NoteEntity noteEntity = noteOpt.get(); + if (!noteEntity.isShared()) { noteEntity.setShared(true); noteEntity.setShareToken(UUID.randomUUID().toString()); @@ -239,7 +241,7 @@ public NoteResponse shareNote(Long noteId) { logger.info("Note " + noteId + " shared with token " + noteEntity.getShareToken()); } - return NoteResponse.fromEntity(noteEntity); + return NoteResponse.fromEntity(noteEntity, getNoteUrl(noteEntity.getId())); } /** @@ -252,7 +254,7 @@ public NoteResponse unshareNote(Long noteId) { UserEntity user = getCurrentUser(); logger.info("Unsharing note " + noteId + " for user " + user.getId()); - Optional noteOpt = noteRepository.findById(noteId); + Optional noteOpt = noteRepository.findByIdAndUser_id(noteId, user.getId()); if (noteOpt.isEmpty()) { throw new NoteNotFoundException(); } @@ -263,7 +265,7 @@ public NoteResponse unshareNote(Long noteId) { noteRepository.save(noteEntity); logger.info("Note " + noteId + " unshared."); - return NoteResponse.fromEntity(noteEntity); + return NoteResponse.fromEntity(noteEntity, getNoteUrl(noteEntity.getId())); } /** @@ -280,7 +282,7 @@ public NoteResponse getSharedNote(String shareToken) { throw new NoteNotFoundException(); } - return NoteResponse.fromEntity(noteOpt.get()); + return NoteResponse.fromEntity(noteOpt.get(), getNoteUrl(noteOpt.get().getId())); } private UserEntity getCurrentUser() { @@ -289,6 +291,22 @@ private UserEntity getCurrentUser() { return authService.findByEmail(email).orElseThrow(); } + private String getNoteUrl(Long noteId) { + return noteUrlRepository.findByNote_id(noteId).map(NoteUrlEntity::getUrl).orElse(null); + } + + private List getNotesUrl(List notes) { + List noteIds = notes.stream().map(NoteEntity::getId).toList(); + if (noteIds.isEmpty()) { + return notes.stream().map(n -> NoteResponse.fromEntity(n, null)).toList(); + } + List urls = noteUrlRepository.findAllByNote_idIn(noteIds); + Map noteUrls = + urls.stream().collect(Collectors.toMap(nu -> nu.getNote().getId(), NoteUrlEntity::getUrl)); + + return notes.stream().map(n -> NoteResponse.fromEntity(n, noteUrls.get(n.getId()))).toList(); + } + private NoteUrlEntity saveUrl(NoteEntity noteEntity, String url) { NoteUrlEntity noteUrl = new NoteUrlEntity(); noteUrl.setUrl(url); diff --git a/server/src/main/java/br/com/tasknoteapp/server/service/impl/UserServiceImpl.java b/server/src/main/java/br/com/tasknoteapp/server/service/impl/UserServiceImpl.java index d2833b9..d461e68 100644 --- a/server/src/main/java/br/com/tasknoteapp/server/service/impl/UserServiceImpl.java +++ b/server/src/main/java/br/com/tasknoteapp/server/service/impl/UserServiceImpl.java @@ -4,7 +4,6 @@ import br.com.tasknoteapp.server.repository.UserRepository; import br.com.tasknoteapp.server.service.UserService; import java.util.Optional; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Service; @@ -12,24 +11,21 @@ @Service class UserServiceImpl implements UserService { - private final UserRepository userRepository; + private final UserDetailsService cachedUserDetailsService; public UserServiceImpl(UserRepository userRepository) { - this.userRepository = userRepository; + this.cachedUserDetailsService = + email -> { + Optional user = userRepository.findByEmail(email); + if (user.isEmpty()) { + throw new RuntimeException("User not found: " + email); + } + return user.get(); + }; } @Override public UserDetailsService userDetailsService() { - return new UserDetailsService() { - @Override - public UserDetails loadUserByUsername(String email) { - Optional user = userRepository.findByEmail(email); - if (user.isEmpty()) { - throw new RuntimeException("User not found: " + email); - } - - return user.get(); - } - }; + return this.cachedUserDetailsService; } } diff --git a/server/src/main/resources/application-native.yml b/server/src/main/resources/application-native.yml index e214b18..1f28ab1 100644 --- a/server/src/main/resources/application-native.yml +++ b/server/src/main/resources/application-native.yml @@ -36,7 +36,8 @@ spring: locations: classpath:db/migration jpa: database-platform: org.hibernate.dialect.PostgreSQLDialect + open-in-view: false properties: hibernate: default_schema: tasknote - show-sql: true + show-sql: false diff --git a/server/src/main/resources/application.yml b/server/src/main/resources/application.yml index e214b18..1f28ab1 100644 --- a/server/src/main/resources/application.yml +++ b/server/src/main/resources/application.yml @@ -36,7 +36,8 @@ spring: locations: classpath:db/migration jpa: database-platform: org.hibernate.dialect.PostgreSQLDialect + open-in-view: false properties: hibernate: default_schema: tasknote - show-sql: true + show-sql: false diff --git a/server/src/test/java/br/com/tasknoteapp/server/controller/NoteControllerTest.java b/server/src/test/java/br/com/tasknoteapp/server/controller/NoteControllerTest.java index a40ddff..729c3f5 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/controller/NoteControllerTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/controller/NoteControllerTest.java @@ -12,7 +12,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; -import br.com.tasknoteapp.server.entity.NoteEntity; import br.com.tasknoteapp.server.exception.NoteNotFoundException; import br.com.tasknoteapp.server.request.NotePatchRequest; import br.com.tasknoteapp.server.request.NoteRequest; @@ -199,10 +198,8 @@ void patchNote_unauthorized_shouldFail() throws Exception { void postNotes_happyPath_shouldSucceed() throws Exception { NoteRequest request = new NoteRequest("Title", "Description", null, null); - NoteEntity entity = new NoteEntity(); - entity.setId(1L); - entity.setTitle(request.title()); - entity.setDescription(request.description()); + NoteResponse entity = new NoteResponse(1L, request.title(), request.description(), + null, null, null, false, null); when(noteService.createNote(request)).thenReturn(entity); @@ -223,9 +220,9 @@ void postNotes_happyPath_shouldSucceed() throws Exception { .accept(MediaType.APPLICATION_JSON) .content(payloadJson)) .andExpect(status().isCreated()) - .andExpect(jsonPath("$.id").value(entity.getId())) - .andExpect(jsonPath("$.title").value(entity.getTitle())) - .andExpect(jsonPath("$.description").value(entity.getDescription())) + .andExpect(jsonPath("$.id").value(entity.id())) + .andExpect(jsonPath("$.title").value(entity.title())) + .andExpect(jsonPath("$.description").value(entity.description())) .andExpect(jsonPath("$.url", Matchers.nullValue())) .andReturn(); } diff --git a/server/src/test/java/br/com/tasknoteapp/server/service/AuthServiceTest.java b/server/src/test/java/br/com/tasknoteapp/server/service/AuthServiceTest.java index e1801ea..f7843bc 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/service/AuthServiceTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/service/AuthServiceTest.java @@ -36,8 +36,6 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.core.env.Environment; -import org.springframework.data.domain.Sort; -import org.springframework.data.domain.Sort.Direction; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.core.userdetails.User; @@ -201,8 +199,8 @@ void signInUser_happyPath_shouldSucceed() { existing.setEmail(request.email()); when(userRepository.findByEmail(request.email())).thenReturn(Optional.of(existing)); - Sort sort = Sort.by(Direction.DESC, "whenHappened"); - when(userPwdLimitRepository.findAllByUser_id(existing.getId(), sort)).thenReturn(List.of()); + when(userPwdLimitRepository.findTop3ByUser_idOrderByWhenHappenedDesc(existing.getId())) + .thenReturn(List.of()); when(authenticationManager.authenticate(any())).thenReturn(null); when(jwtService.generateToken(existing)).thenReturn("a1b2c3"); @@ -241,8 +239,7 @@ void signInUser_maxLoginAttempt_shouldFail() { limit1.setWhenHappened(LocalDateTime.now().minusMinutes(1)); UserPwdLimitEntity limit2 = new UserPwdLimitEntity(); UserPwdLimitEntity limit3 = new UserPwdLimitEntity(); - Sort sort = Sort.by(Direction.DESC, "whenHappened"); - when(userPwdLimitRepository.findAllByUser_id(existing.getId(), sort)) + when(userPwdLimitRepository.findTop3ByUser_idOrderByWhenHappenedDesc(existing.getId())) .thenReturn(List.of(limit1, limit2, limit3)); Assertions.assertThrows( @@ -261,8 +258,8 @@ void signInUser_badCredentials_shouldFail() { existing.setId(919L); when(userRepository.findByEmail(request.email())).thenReturn(Optional.of(existing)); - Sort sort = Sort.by(Direction.DESC, "whenHappened"); - when(userPwdLimitRepository.findAllByUser_id(existing.getId(), sort)).thenReturn(List.of()); + when(userPwdLimitRepository.findTop3ByUser_idOrderByWhenHappenedDesc(existing.getId())) + .thenReturn(List.of()); when(authenticationManager.authenticate(any())).thenThrow(new BadCredentialsException("Wrong")); UserResponseWithToken token = authService.signInUser(request); diff --git a/server/src/test/java/br/com/tasknoteapp/server/service/MailgunEmailServiceTest.java b/server/src/test/java/br/com/tasknoteapp/server/service/MailgunEmailServiceTest.java index 8d1e3f7..9afb562 100644 --- a/server/src/test/java/br/com/tasknoteapp/server/service/MailgunEmailServiceTest.java +++ b/server/src/test/java/br/com/tasknoteapp/server/service/MailgunEmailServiceTest.java @@ -30,14 +30,15 @@ class MailgunEmailServiceTest { @BeforeEach void setUp() { + when(restTemplateBuilder.connectTimeout(any())).thenReturn(restTemplateBuilder); + when(restTemplateBuilder.readTimeout(any())).thenReturn(restTemplateBuilder); + when(restTemplateBuilder.defaultHeader(any(), any())).thenReturn(restTemplateBuilder); + when(restTemplateBuilder.build()).thenReturn(restTemplate); + String apiKey = "abx123"; String domain = "domain.com"; String sender = "no-reply@domain.com"; String target = "development"; - - when(restTemplateBuilder.defaultHeader(any(), any())).thenReturn(restTemplateBuilder); - when(restTemplateBuilder.build()).thenReturn(restTemplate); - mailgunEmailService = new MailgunEmailService(apiKey, domain, sender, target, restTemplateBuilder); } 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 7f0e756..2b6cda3 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 @@ -3,8 +3,8 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.eq; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -105,9 +105,9 @@ void createNote_withExistingCount() { when(noteRepository.save(any(NoteEntity.class))).thenReturn(note); when(noteUrlRepository.save(any(NoteUrlEntity.class))).thenReturn(new NoteUrlEntity()); - NoteEntity createdNote = noteService.createNote(noteRequest); + NoteResponse createdNote = noteService.createNote(noteRequest); - assertEquals("Test Note", createdNote.getTitle()); + assertEquals("Test Note", createdNote.title()); verify(noteRepository, times(1)).save(any(NoteEntity.class)); verify(noteUrlRepository, times(1)).save(any(NoteUrlEntity.class)); } @@ -116,13 +116,14 @@ void createNote_withExistingCount() { void patchNote() { when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); - when(noteRepository.findById(note.getId())).thenReturn(Optional.of(note)); + when(noteRepository.findByIdAndUser_id(note.getId(), user.getId())) + .thenReturn(Optional.of(note)); when(noteRepository.save(any(NoteEntity.class))).thenReturn(note); NoteResponse patchedNote = noteService.patchNote(note.getId(), notePatchRequest); assertEquals("Updated Note", patchedNote.title()); - verify(noteRepository, times(1)).findById(note.getId()); + verify(noteRepository, times(1)).findByIdAndUser_id(note.getId(), user.getId()); verify(noteRepository, times(1)).save(any(NoteEntity.class)); } @@ -130,7 +131,8 @@ void patchNote() { void patchNote_notFound() { when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); - when(noteRepository.findById(note.getId())).thenReturn(Optional.empty()); + when(noteRepository.findByIdAndUser_id(note.getId(), user.getId())) + .thenReturn(Optional.empty()); Long noteId = note.getId(); assertThrows( @@ -141,11 +143,12 @@ void patchNote_notFound() { void deleteNote() { when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); - when(noteRepository.findById(note.getId())).thenReturn(Optional.of(note)); + when(noteRepository.findByIdAndUser_id(note.getId(), user.getId())) + .thenReturn(Optional.of(note)); noteService.deleteNote(note.getId()); - verify(noteRepository, times(1)).findById(note.getId()); + verify(noteRepository, times(1)).findByIdAndUser_id(note.getId(), user.getId()); verify(noteRepository, times(1)).delete(note); } @@ -167,7 +170,8 @@ void searchNotes() { void shareNote() { when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); - when(noteRepository.findById(note.getId())).thenReturn(Optional.of(note)); + when(noteRepository.findByIdAndUser_id(note.getId(), user.getId())) + .thenReturn(Optional.of(note)); when(noteRepository.save(any(NoteEntity.class))).thenReturn(note); NoteResponse response = noteService.shareNote(note.getId()); @@ -180,7 +184,8 @@ void shareNote() { void shareNote_notFound() { when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); - when(noteRepository.findById(note.getId())).thenReturn(Optional.empty()); + when(noteRepository.findByIdAndUser_id(note.getId(), user.getId())) + .thenReturn(Optional.empty()); Long noteId = note.getId(); assertThrows(NoteNotFoundException.class, () -> noteService.shareNote(noteId)); @@ -192,7 +197,8 @@ void unshareNote() { note.setShareToken("some-token"); when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); - when(noteRepository.findById(note.getId())).thenReturn(Optional.of(note)); + when(noteRepository.findByIdAndUser_id(note.getId(), user.getId())) + .thenReturn(Optional.of(note)); when(noteRepository.save(any(NoteEntity.class))).thenReturn(note); NoteResponse response = noteService.unshareNote(note.getId()); @@ -205,7 +211,8 @@ void unshareNote() { void unshareNote_notFound() { when(authUtil.getCurrentUserEmail()).thenReturn(Optional.of(user.getEmail())); when(authService.findByEmail(user.getEmail())).thenReturn(Optional.of(user)); - when(noteRepository.findById(note.getId())).thenReturn(Optional.empty()); + when(noteRepository.findByIdAndUser_id(note.getId(), user.getId())) + .thenReturn(Optional.empty()); Long noteId = note.getId(); assertThrows(NoteNotFoundException.class, () -> noteService.unshareNote(noteId));