diff --git a/backend/lined/src/main/java/io/backend/lined/common/exception/BadRequestException.java b/backend/lined/src/main/java/io/backend/lined/common/exception/BadRequestException.java new file mode 100644 index 0000000..9fe12de --- /dev/null +++ b/backend/lined/src/main/java/io/backend/lined/common/exception/BadRequestException.java @@ -0,0 +1,10 @@ +package io.backend.lined.common.exception; + +import org.springframework.http.HttpStatus; + +public class BadRequestException extends BaseAppException { + + public BadRequestException(String message) { + super(HttpStatus.BAD_REQUEST, "common.bad_request", message); + } +} diff --git a/backend/lined/src/main/java/io/backend/lined/common/exception/ForbiddenException.java b/backend/lined/src/main/java/io/backend/lined/common/exception/ForbiddenException.java new file mode 100644 index 0000000..778a6a1 --- /dev/null +++ b/backend/lined/src/main/java/io/backend/lined/common/exception/ForbiddenException.java @@ -0,0 +1,10 @@ +package io.backend.lined.common.exception; + +import org.springframework.http.HttpStatus; + +public class ForbiddenException extends BaseAppException { + + public ForbiddenException(String message) { + super(HttpStatus.FORBIDDEN, "common.forbidden", message); + } +} diff --git a/backend/lined/src/main/java/io/backend/lined/config/GlobalExceptionHandler.java b/backend/lined/src/main/java/io/backend/lined/config/GlobalExceptionHandler.java index 0d345d0..ea8d6c2 100644 --- a/backend/lined/src/main/java/io/backend/lined/config/GlobalExceptionHandler.java +++ b/backend/lined/src/main/java/io/backend/lined/config/GlobalExceptionHandler.java @@ -21,6 +21,7 @@ public ResponseEntity handleBase(BaseAppException ex) { case NOT_FOUND -> "Resource not found"; case CONFLICT -> "Conflict"; case BAD_REQUEST -> "Bad request"; + case FORBIDDEN -> "Forbidden"; default -> ex.getStatus().getReasonPhrase(); }); pd.setType(URI.create("https://errors.lined.app/" + ex.getCode())); diff --git a/backend/lined/src/main/java/io/backend/lined/event/domain/EventEntity.java b/backend/lined/src/main/java/io/backend/lined/event/domain/EventEntity.java index a386047..975436e 100644 --- a/backend/lined/src/main/java/io/backend/lined/event/domain/EventEntity.java +++ b/backend/lined/src/main/java/io/backend/lined/event/domain/EventEntity.java @@ -66,9 +66,6 @@ void onCreate() { if (createdAt == null) { createdAt = OffsetDateTime.now(); } - if (startAt != null && endAt != null && !startAt.isBefore(endAt)) { - throw new IllegalArgumentException("startAt must be before endAt"); - } } } diff --git a/backend/lined/src/main/java/io/backend/lined/event/service/EventService.java b/backend/lined/src/main/java/io/backend/lined/event/service/EventService.java index f5d88fa..cfc17d1 100644 --- a/backend/lined/src/main/java/io/backend/lined/event/service/EventService.java +++ b/backend/lined/src/main/java/io/backend/lined/event/service/EventService.java @@ -1,5 +1,8 @@ package io.backend.lined.event.service; +import io.backend.lined.common.exception.BadRequestException; +import io.backend.lined.common.exception.ForbiddenException; +import io.backend.lined.common.exception.NotFoundException; import io.backend.lined.event.api.EventConflictDto; import io.backend.lined.event.api.EventCreateDto; import io.backend.lined.event.api.EventDto; @@ -21,9 +24,9 @@ public interface EventService { * @param dto the event creation data * @param currentUserId the ID of the user creating the event * @return the created event as a DTO - * @throws IllegalArgumentException if startAt is not before endAt - * @throws SecurityException if the user is not a lobby member - * @throws NoSuchElementException if the lobby or user is not found + * @throws BadRequestException if startAt is not before endAt + * @throws ForbiddenException if the user is not a lobby member + * @throws NotFoundException if the lobby or user is not found */ EventDto create(EventCreateDto dto, Long currentUserId); @@ -35,9 +38,9 @@ public interface EventService { * @param dto the update data * @param currentUserId the ID of the requesting user * @return the updated event as a DTO - * @throws IllegalArgumentException if the updated dates are invalid - * @throws SecurityException if the user is not a lobby member - * @throws NoSuchElementException if the event is not found + * @throws BadRequestException if the updated dates are invalid + * @throws ForbiddenException if the user is not a lobby member + * @throws NotFoundException if the event is not found */ EventDto update(Long id, EventUpdateDto dto, Long currentUserId); @@ -46,8 +49,8 @@ public interface EventService { * * @param id the event ID to delete * @param currentUserId the ID of the requesting user - * @throws SecurityException if the user is not a lobby member - * @throws NoSuchElementException if the event is not found + * @throws ForbiddenException if the user is not a lobby member + * @throws NotFoundException if the event is not found */ void delete(Long id, Long currentUserId); @@ -59,8 +62,8 @@ public interface EventService { * @param to the end of the time window (exclusive) * @param currentUserId the ID of the requesting user * @return a list of events ordered by start time - * @throws IllegalArgumentException if the time window is invalid - * @throws SecurityException if the user is not a lobby member + * @throws BadRequestException if the time window is invalid + * @throws ForbiddenException if the user is not a lobby member */ List list(Long lobbyId, OffsetDateTime from, OffsetDateTime to, Long currentUserId); @@ -72,8 +75,8 @@ public interface EventService { * @param end the end of the time window * @param requesterId the ID of the requesting user * @return a list of conflicting event pairs with overlap bounds - * @throws IllegalArgumentException if start is not before end - * @throws SecurityException if the user is not a lobby member + * @throws BadRequestException if start is not before end + * @throws ForbiddenException if the user is not a lobby member */ List findConflicts(Long lobbyId, OffsetDateTime start, OffsetDateTime end, Long requesterId); @@ -86,7 +89,7 @@ List findConflicts(Long lobbyId, OffsetDateTime start, * @param end the end of the time window * @param requesterId the ID of the requesting user * @return a conflict result with whether a conflict exists and which event causes it - * @throws IllegalArgumentException if start is not before end + * @throws BadRequestException if start is not before end */ UserConflictDto hasConflict(Long userId, OffsetDateTime start, OffsetDateTime end, Long requesterId); diff --git a/backend/lined/src/main/java/io/backend/lined/event/service/EventServiceImpl.java b/backend/lined/src/main/java/io/backend/lined/event/service/EventServiceImpl.java index aba5161..ca225ac 100644 --- a/backend/lined/src/main/java/io/backend/lined/event/service/EventServiceImpl.java +++ b/backend/lined/src/main/java/io/backend/lined/event/service/EventServiceImpl.java @@ -1,5 +1,8 @@ package io.backend.lined.event.service; +import io.backend.lined.common.EntityFinder; +import io.backend.lined.common.exception.BadRequestException; +import io.backend.lined.common.exception.NotFoundException; import io.backend.lined.event.api.EventConflictDto; import io.backend.lined.event.api.EventCreateDto; import io.backend.lined.event.api.EventDto; @@ -17,7 +20,6 @@ import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; -import java.util.NoSuchElementException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -39,7 +41,7 @@ public EventDto create(EventCreateDto dto, Long currentUserId) { accessPolicy.ensureMember(lobby, currentUserId); if (!dto.startAt().isBefore(dto.endAt())) { - throw new IllegalArgumentException("startAt must be before endAt"); + throw new BadRequestException("startAt must be before endAt"); } var entity = EventEntity.builder() @@ -77,7 +79,7 @@ public EventDto update(Long id, EventUpdateDto dto, Long currentUserId) { } if (!e.getStartAt().isBefore(e.getEndAt())) { - throw new IllegalArgumentException("startAt must be before endAt"); + throw new BadRequestException("startAt must be before endAt"); } return mapper.toDto(e); @@ -97,7 +99,7 @@ public List list(Long lobbyId, OffsetDateTime from, OffsetDateTime to, accessPolicy.ensureMember(lobby, currentUserId); if (from == null || to == null || !from.isBefore(to)) { - throw new IllegalArgumentException("Invalid time window: from < to is required"); + throw new BadRequestException("Invalid time window: from < to is required"); } return repo.findOverlapping(lobbyId, from, to).stream().map(mapper::toDto).toList(); @@ -110,7 +112,7 @@ public List findConflicts(Long lobbyId, var lobby = mustLobby(lobbyId); accessPolicy.ensureMember(lobby, requesterId); if (!start.isBefore(end)) { - throw new IllegalArgumentException("start must be before end"); + throw new BadRequestException("start must be before end"); } var events = repo.findOverlapping(lobbyId, start, end); List conflicts = new ArrayList<>(); @@ -135,7 +137,7 @@ public List findConflicts(Long lobbyId, public UserConflictDto hasConflict(Long userId, OffsetDateTime start, OffsetDateTime end, Long requesterId) { if (!start.isBefore(end)) { - throw new IllegalArgumentException("start must be before end"); + throw new BadRequestException("start must be before end"); } var overlapping = repo.findOverlappingByUser(userId, start, end); if (overlapping.isEmpty()) { @@ -145,18 +147,18 @@ public UserConflictDto hasConflict(Long userId, OffsetDateTime start, } private UserEntity mustUser(Long id) { - return userRepo.findById(id) - .orElseThrow(() -> new NoSuchElementException("User %d not found".formatted(id))); + return EntityFinder.findOrThrow(userRepo.findById(id), + () -> new NotFoundException("User %d not found".formatted(id))); } private LobbyEntity mustLobby(Long id) { - return lobbyRepo.findById(id) - .orElseThrow(() -> new NoSuchElementException("Lobby %d not found".formatted(id))); + return EntityFinder.findOrThrow(lobbyRepo.findById(id), + () -> new NotFoundException("Lobby %d not found".formatted(id))); } private EventEntity mustEvent(Long id) { - return repo.findById(id) - .orElseThrow(() -> new NoSuchElementException("Event %d not found".formatted(id))); + return EntityFinder.findOrThrow(repo.findById(id), + () -> new NotFoundException("Event %d not found".formatted(id))); } } diff --git a/backend/lined/src/main/java/io/backend/lined/lobby/service/LobbyAccessPolicy.java b/backend/lined/src/main/java/io/backend/lined/lobby/service/LobbyAccessPolicy.java index df6b99c..ec86d47 100644 --- a/backend/lined/src/main/java/io/backend/lined/lobby/service/LobbyAccessPolicy.java +++ b/backend/lined/src/main/java/io/backend/lined/lobby/service/LobbyAccessPolicy.java @@ -1,5 +1,6 @@ package io.backend.lined.lobby.service; +import io.backend.lined.common.exception.ForbiddenException; import io.backend.lined.lobby.domain.LobbyEntity; import java.util.Objects; import org.springframework.stereotype.Component; @@ -13,6 +14,7 @@ public class LobbyAccessPolicy { * are initialized (i.e. called within an active transaction). */ public void ensureMember(LobbyEntity lobby, Long userId) { + Objects.requireNonNull(lobby, "lobby must not be null"); Objects.requireNonNull(userId, "userId must not be null"); if (lobby.getOwner().getId().equals(userId)) { return; @@ -20,7 +22,7 @@ public void ensureMember(LobbyEntity lobby, Long userId) { boolean isMember = lobby.getMembers().stream() .anyMatch(u -> u.getId().equals(userId)); if (!isMember) { - throw new SecurityException("User is not a member of the lobby"); + throw new ForbiddenException("User is not a member of the lobby"); } } @@ -29,9 +31,10 @@ public void ensureMember(LobbyEntity lobby, Long userId) { * Callers must ensure {@code lobby.getOwner()} is initialized. */ public void ensureOwner(LobbyEntity lobby, Long requesterId) { + Objects.requireNonNull(lobby, "lobby must not be null"); Objects.requireNonNull(requesterId, "requesterId must not be null"); if (!lobby.getOwner().getId().equals(requesterId)) { - throw new SecurityException("Only lobby owner can perform this action"); + throw new ForbiddenException("Only lobby owner can perform this action"); } } diff --git a/backend/lined/src/main/java/io/backend/lined/lobby/service/LobbyServiceImpl.java b/backend/lined/src/main/java/io/backend/lined/lobby/service/LobbyServiceImpl.java index 747e0ab..ad60ce2 100644 --- a/backend/lined/src/main/java/io/backend/lined/lobby/service/LobbyServiceImpl.java +++ b/backend/lined/src/main/java/io/backend/lined/lobby/service/LobbyServiceImpl.java @@ -1,6 +1,8 @@ package io.backend.lined.lobby.service; import io.backend.lined.common.EntityFinder; +import io.backend.lined.common.exception.BadRequestException; +import io.backend.lined.common.exception.NotFoundException; import io.backend.lined.lobby.api.LobbyCreateDto; import io.backend.lined.lobby.api.LobbyDto; import io.backend.lined.lobby.api.LobbyMapper; @@ -9,7 +11,6 @@ import io.backend.lined.user.domain.UserRepository; import jakarta.transaction.Transactional; import java.util.List; -import java.util.NoSuchElementException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -25,8 +26,8 @@ public class LobbyServiceImpl implements LobbyService { @Override public LobbyDto create(LobbyCreateDto dto, Long ownerId) { - var owner = userRepo.findById(ownerId) - .orElseThrow(() -> new NoSuchElementException("Owner %d not found".formatted(ownerId))); + var owner = EntityFinder.findOrThrow(userRepo.findById(ownerId), + () -> new NotFoundException("Owner %d not found".formatted(ownerId))); var entity = LobbyEntity.builder() .name(dto.name()) @@ -58,8 +59,8 @@ public LobbyDto addMember(Long lobbyId, Long userIdToAdd, Long requesterId) { accessPolicy.ensureOwner(lobby, requesterId); - var user = userRepo.findById(userIdToAdd) - .orElseThrow(() -> new NoSuchElementException("User %d not found".formatted(userIdToAdd))); + var user = EntityFinder.findOrThrow(userRepo.findById(userIdToAdd), + () -> new NotFoundException("User %d not found".formatted(userIdToAdd))); lobby.getMembers().add(user); return mapper.toDto(lobby); @@ -72,7 +73,7 @@ public LobbyDto removeMember(Long lobbyId, Long userIdToRemove, Long requesterId accessPolicy.ensureOwner(lobby, requesterId); if (lobby.getOwner().getId().equals(userIdToRemove)) { - throw new IllegalArgumentException("Owner cannot be removed from lobby"); + throw new BadRequestException("Owner cannot be removed from lobby"); } lobby.getMembers().removeIf(u -> u.getId().equals(userIdToRemove)); @@ -89,7 +90,7 @@ public void delete(Long lobbyId, Long requesterId) { private LobbyEntity mustLobby(Long id) { return EntityFinder.findOrThrow( lobbyRepo.findById(id), - () -> new NoSuchElementException("Lobby %d not found".formatted(id))); + () -> new NotFoundException("Lobby %d not found".formatted(id))); } } diff --git a/backend/lined/src/main/java/io/backend/lined/task/service/TaskService.java b/backend/lined/src/main/java/io/backend/lined/task/service/TaskService.java index d7c392a..3131602 100644 --- a/backend/lined/src/main/java/io/backend/lined/task/service/TaskService.java +++ b/backend/lined/src/main/java/io/backend/lined/task/service/TaskService.java @@ -1,6 +1,8 @@ package io.backend.lined.task.service; - +import io.backend.lined.common.exception.BadRequestException; +import io.backend.lined.common.exception.ForbiddenException; +import io.backend.lined.common.exception.NotFoundException; import io.backend.lined.task.api.TaskCreateDto; import io.backend.lined.task.api.TaskDto; import io.backend.lined.task.api.TaskUpdateDto; @@ -18,8 +20,8 @@ public interface TaskService { * @param dto the task creation data * @param currentUserId the ID of the user creating the task * @return the created task as a DTO - * @throws SecurityException if the user is not a lobby member - * @throws NoSuchElementException if the lobby or user is not found + * @throws ForbiddenException if the user is not a lobby member + * @throws NotFoundException if the lobby or user is not found */ TaskDto create(TaskCreateDto dto, Long currentUserId); @@ -31,8 +33,8 @@ public interface TaskService { * @param dto the update data * @param currentUserId the ID of the requesting user * @return the updated task as a DTO - * @throws SecurityException if the user is not a lobby member - * @throws NoSuchElementException if the task is not found + * @throws ForbiddenException if the user is not a lobby member + * @throws NotFoundException if the task is not found */ TaskDto update(Long id, TaskUpdateDto dto, Long currentUserId); @@ -41,8 +43,8 @@ public interface TaskService { * * @param id the task ID to delete * @param currentUserId the ID of the requesting user - * @throws SecurityException if the user is not a lobby member - * @throws NoSuchElementException if the task is not found + * @throws ForbiddenException if the user is not a lobby member + * @throws NotFoundException if the task is not found */ void delete(Long id, Long currentUserId); @@ -52,8 +54,9 @@ public interface TaskService { * * @param lobbyId the lobby ID to filter by, or null for all lobbies * @param assigneeId the assignee user ID to filter by, or null for all assignees - * @param status the task status to filter by (e.g. "TO DO", "DONE"), or null for all statuses + * @param status the task status string to filter by (e.g. "TODO", "DONE"), or null for all * @return a list of matching tasks + * @throws BadRequestException if {@code status} is not a valid {@link io.backend.lined.task.domain.TaskStatus} value */ List list(Long lobbyId, Long assigneeId, String status); diff --git a/backend/lined/src/main/java/io/backend/lined/task/service/TaskServiceImpl.java b/backend/lined/src/main/java/io/backend/lined/task/service/TaskServiceImpl.java index e76a6d9..575e3eb 100644 --- a/backend/lined/src/main/java/io/backend/lined/task/service/TaskServiceImpl.java +++ b/backend/lined/src/main/java/io/backend/lined/task/service/TaskServiceImpl.java @@ -1,6 +1,8 @@ package io.backend.lined.task.service; import io.backend.lined.common.EntityFinder; +import io.backend.lined.common.exception.BadRequestException; +import io.backend.lined.common.exception.NotFoundException; import io.backend.lined.lobby.domain.LobbyEntity; import io.backend.lined.lobby.domain.LobbyRepository; import io.backend.lined.lobby.service.LobbyAccessPolicy; @@ -15,7 +17,6 @@ import io.backend.lined.user.domain.UserRepository; import jakarta.transaction.Transactional; import java.util.List; -import java.util.NoSuchElementException; import lombok.RequiredArgsConstructor; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; @@ -89,7 +90,12 @@ public List list(Long lobbyId, Long assigneeId, String status) { spec = spec.and((root, q, cb) -> cb.equal(root.get("assignee").get("id"), assigneeId)); } if (status != null) { - var st = TaskStatus.valueOf(status); + TaskStatus st; + try { + st = TaskStatus.valueOf(status); + } catch (IllegalArgumentException e) { + throw new BadRequestException("Invalid status value: " + status); + } spec = spec.and((root, q, cb) -> cb.equal(root.get("status"), st)); } @@ -97,18 +103,18 @@ public List list(Long lobbyId, Long assigneeId, String status) { } private UserEntity mustUser(Long id) { - return userRepo.findById(id) - .orElseThrow(() -> new NoSuchElementException("User %d not found".formatted(id))); + return EntityFinder.findOrThrow(userRepo.findById(id), + () -> new NotFoundException("User %d not found".formatted(id))); } private LobbyEntity mustLobby(Long id) { - return lobbyRepo.findById(id) - .orElseThrow(() -> new NoSuchElementException("Lobby %d not found".formatted(id))); + return EntityFinder.findOrThrow(lobbyRepo.findById(id), + () -> new NotFoundException("Lobby %d not found".formatted(id))); } private TaskEntity mustTask(Long id) { return EntityFinder.findOrThrow(repo.findById(id), - () -> new NoSuchElementException("Task %d not found".formatted(id))); + () -> new NotFoundException("Task %d not found".formatted(id))); } } diff --git a/backend/lined/src/test/java/io/backend/lined/event/service/EventServiceImplConflictTest.java b/backend/lined/src/test/java/io/backend/lined/event/service/EventServiceImplConflictTest.java index 7a5e37a..5a66fdd 100644 --- a/backend/lined/src/test/java/io/backend/lined/event/service/EventServiceImplConflictTest.java +++ b/backend/lined/src/test/java/io/backend/lined/event/service/EventServiceImplConflictTest.java @@ -8,6 +8,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.backend.lined.common.exception.BadRequestException; +import io.backend.lined.common.exception.ForbiddenException; +import io.backend.lined.common.exception.NotFoundException; import io.backend.lined.event.api.EventConflictDto; import io.backend.lined.event.api.EventDto; import io.backend.lined.event.api.EventMapper; @@ -23,7 +26,6 @@ import java.time.OffsetDateTime; import java.util.HashSet; import java.util.List; -import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.BeforeEach; @@ -230,36 +232,36 @@ void findConflicts_calculatesOverlapBoundsCorrectly() { } @Test - void findConflicts_throwsIllegalArgument_whenStartAfterEnd() { + void findConflicts_throwsBadRequest_whenStartAfterEnd() { when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobby)); assertThatThrownBy(() -> eventService.findConflicts(101L, windowEnd, windowStart, 1L)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(BadRequestException.class) .hasMessageContaining("start must be before end"); verify(repo, never()).findOverlapping(any(), any(), any()); } @Test - void findConflicts_throwsSecurity_whenUserIsNotLobbyMember() { + void findConflicts_throwsForbidden_whenUserIsNotLobbyMember() { when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobby)); assertThatThrownBy(() -> eventService.findConflicts(101L, windowStart, windowEnd, 99L)) - .isInstanceOf(SecurityException.class) + .isInstanceOf(ForbiddenException.class) .hasMessageContaining("not a member"); verify(repo, never()).findOverlapping(any(), any(), any()); } @Test - void findConflicts_throwsNoSuchElement_whenLobbyNotFound() { + void findConflicts_throwsNotFound_whenLobbyNotFound() { when(lobbyRepo.findById(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> eventService.findConflicts(999L, windowStart, windowEnd, 1L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("999"); } @@ -303,20 +305,20 @@ void hasConflict_returnsFirstConflict_whenMultipleOverlap() { } @Test - void hasConflict_throwsIllegalArgument_whenStartAfterEnd() { + void hasConflict_throwsBadRequest_whenStartAfterEnd() { assertThatThrownBy(() -> eventService.hasConflict(1L, windowEnd, windowStart, 1L)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(BadRequestException.class) .hasMessageContaining("start must be before end"); verify(repo, never()).findOverlappingByUser(any(), any(), any()); } @Test - void hasConflict_throwsIllegalArgument_whenStartEqualsEnd() { + void hasConflict_throwsBadRequest_whenStartEqualsEnd() { assertThatThrownBy(() -> eventService.hasConflict(1L, windowStart, windowStart, 1L)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(BadRequestException.class) .hasMessageContaining("start must be before end"); } @@ -330,4 +332,4 @@ void hasConflict_includesUserIdInResponse() { assertThat(result.userId()).isEqualTo(2L); assertThat(result.hasConflict()).isFalse(); } -} \ No newline at end of file +} diff --git a/backend/lined/src/test/java/io/backend/lined/event/service/EventServiceImplTest.java b/backend/lined/src/test/java/io/backend/lined/event/service/EventServiceImplTest.java index 8cd55e4..a551d6c 100644 --- a/backend/lined/src/test/java/io/backend/lined/event/service/EventServiceImplTest.java +++ b/backend/lined/src/test/java/io/backend/lined/event/service/EventServiceImplTest.java @@ -7,6 +7,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.backend.lined.common.exception.BadRequestException; +import io.backend.lined.common.exception.ForbiddenException; +import io.backend.lined.common.exception.NotFoundException; import io.backend.lined.event.api.EventCreateDto; import io.backend.lined.event.api.EventDto; import io.backend.lined.event.api.EventMapper; @@ -20,10 +23,9 @@ import io.backend.lined.user.domain.UserEntity; import io.backend.lined.user.domain.UserRepository; import java.time.OffsetDateTime; -import java.util.HashSet; import java.util.List; -import java.util.NoSuchElementException; import java.util.Optional; +import java.util.HashSet; import java.util.Set; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -120,7 +122,7 @@ void create_success() { } @Test - void create_throwsIllegalArgument_whenStartAtNotBeforeEndAt() { + void create_throwsBadRequest_whenStartAtNotBeforeEndAt() { EventCreateDto dto = new EventCreateDto( "Dinner together", true, endAt, startAt, "Europe/Kyiv", 101L); @@ -128,14 +130,14 @@ void create_throwsIllegalArgument_whenStartAtNotBeforeEndAt() { when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobby)); assertThatThrownBy(() -> eventService.create(dto, 1L)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(BadRequestException.class) .hasMessageContaining("startAt must be before endAt"); verify(repo, never()).save(any()); } @Test - void create_throwsIllegalArgument_whenStartAtEqualsEndAt() { + void create_throwsBadRequest_whenStartAtEqualsEndAt() { EventCreateDto dto = new EventCreateDto( "Dinner together", true, startAt, startAt, "Europe/Kyiv", 101L); @@ -143,26 +145,26 @@ void create_throwsIllegalArgument_whenStartAtEqualsEndAt() { when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobby)); assertThatThrownBy(() -> eventService.create(dto, 1L)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(BadRequestException.class) .hasMessageContaining("startAt must be before endAt"); } @Test - void create_throwsNoSuchElement_whenUserNotFound() { + void create_throwsNotFound_whenUserNotFound() { EventCreateDto dto = new EventCreateDto( "Dinner together", true, startAt, endAt, "Europe/Kyiv", 101L); when(userRepo.findById(99L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> eventService.create(dto, 99L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("99"); verify(repo, never()).save(any()); } @Test - void create_throwsNoSuchElement_whenLobbyNotFound() { + void create_throwsNotFound_whenLobbyNotFound() { EventCreateDto dto = new EventCreateDto( "Dinner together", true, startAt, endAt, "Europe/Kyiv", 999L); @@ -170,12 +172,12 @@ void create_throwsNoSuchElement_whenLobbyNotFound() { when(lobbyRepo.findById(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> eventService.create(dto, 1L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("999"); } @Test - void create_throwsSecurity_whenUserIsNotLobbyMember() { + void create_throwsForbidden_whenUserIsNotLobbyMember() { UserEntity outsider = new UserEntity(); outsider.setId(99L); @@ -186,7 +188,7 @@ void create_throwsSecurity_whenUserIsNotLobbyMember() { when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobby)); assertThatThrownBy(() -> eventService.create(dto, 99L)) - .isInstanceOf(SecurityException.class) + .isInstanceOf(ForbiddenException.class) .hasMessageContaining("not a member"); } @@ -237,35 +239,35 @@ void update_updatesOnlyNonNullFields() { } @Test - void update_throwsIllegalArgument_whenUpdatedDatesAreInvalid() { + void update_throwsBadRequest_whenUpdatedDatesAreInvalid() { EventUpdateDto dto = new EventUpdateDto(null, null, endAt, startAt, null); when(repo.findById(9001L)).thenReturn(Optional.of(eventEntity)); assertThatThrownBy(() -> eventService.update(9001L, dto, 1L)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(BadRequestException.class) .hasMessageContaining("startAt must be before endAt"); } @Test - void update_throwsNoSuchElement_whenEventNotFound() { + void update_throwsNotFound_whenEventNotFound() { EventUpdateDto dto = new EventUpdateDto("Title", null, null, null, null); when(repo.findById(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> eventService.update(999L, dto, 1L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("999"); } @Test - void update_throwsSecurity_whenUserIsNotLobbyMember() { + void update_throwsForbidden_whenUserIsNotLobbyMember() { EventUpdateDto dto = new EventUpdateDto("Title", null, null, null, null); when(repo.findById(9001L)).thenReturn(Optional.of(eventEntity)); assertThatThrownBy(() -> eventService.update(9001L, dto, 99L)) - .isInstanceOf(SecurityException.class) + .isInstanceOf(ForbiddenException.class) .hasMessageContaining("not a member"); } @@ -283,22 +285,22 @@ void delete_success() { } @Test - void delete_throwsNoSuchElement_whenEventNotFound() { + void delete_throwsNotFound_whenEventNotFound() { when(repo.findById(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> eventService.delete(999L, 1L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("999"); verify(repo, never()).delete(any(EventEntity.class)); } @Test - void delete_throwsSecurity_whenUserIsNotLobbyMember() { + void delete_throwsForbidden_whenUserIsNotLobbyMember() { when(repo.findById(9001L)).thenReturn(Optional.of(eventEntity)); assertThatThrownBy(() -> eventService.delete(9001L, 99L)) - .isInstanceOf(SecurityException.class) + .isInstanceOf(ForbiddenException.class) .hasMessageContaining("not a member"); verify(repo, never()).delete(any(EventEntity.class)); @@ -330,47 +332,47 @@ void list_returnsEmpty_whenNoEventsInWindow() { } @Test - void list_throwsIllegalArgument_whenFromIsNull() { + void list_throwsBadRequest_whenFromIsNull() { when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobby)); assertThatThrownBy(() -> eventService.list(101L, null, endAt, 1L)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(BadRequestException.class) .hasMessageContaining("Invalid time window"); } @Test - void list_throwsIllegalArgument_whenToIsNull() { + void list_throwsBadRequest_whenToIsNull() { when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobby)); assertThatThrownBy(() -> eventService.list(101L, startAt, null, 1L)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(BadRequestException.class) .hasMessageContaining("Invalid time window"); } @Test - void list_throwsIllegalArgument_whenFromIsAfterTo() { + void list_throwsBadRequest_whenFromIsAfterTo() { when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobby)); assertThatThrownBy(() -> eventService.list(101L, endAt, startAt, 1L)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(BadRequestException.class) .hasMessageContaining("Invalid time window"); } @Test - void list_throwsNoSuchElement_whenLobbyNotFound() { + void list_throwsNotFound_whenLobbyNotFound() { when(lobbyRepo.findById(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> eventService.list(999L, startAt, endAt, 1L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("999"); } @Test - void list_throwsSecurity_whenUserIsNotLobbyMember() { + void list_throwsForbidden_whenUserIsNotLobbyMember() { when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobby)); assertThatThrownBy(() -> eventService.list(101L, startAt, endAt, 99L)) - .isInstanceOf(SecurityException.class) + .isInstanceOf(ForbiddenException.class) .hasMessageContaining("not a member"); } -} \ No newline at end of file +} diff --git a/backend/lined/src/test/java/io/backend/lined/lobby/service/LobbyAccessPolicyTest.java b/backend/lined/src/test/java/io/backend/lined/lobby/service/LobbyAccessPolicyTest.java index e2cebb0..cafa315 100644 --- a/backend/lined/src/test/java/io/backend/lined/lobby/service/LobbyAccessPolicyTest.java +++ b/backend/lined/src/test/java/io/backend/lined/lobby/service/LobbyAccessPolicyTest.java @@ -3,6 +3,7 @@ import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import io.backend.lined.common.exception.ForbiddenException; import io.backend.lined.lobby.domain.LobbyEntity; import io.backend.lined.lobby.domain.LobbyTypes; import io.backend.lined.user.domain.UserEntity; @@ -63,9 +64,9 @@ void ensureMember_doesNotThrow_whenUserIsOwnerNotInMembersSet() { } @Test - void ensureMember_throwsSecurity_whenUserIsOutsider() { + void ensureMember_throwsForbidden_whenUserIsOutsider() { assertThatThrownBy(() -> policy.ensureMember(lobby, 99L)) - .isInstanceOf(SecurityException.class) + .isInstanceOf(ForbiddenException.class) .hasMessageContaining("not a member"); } @@ -76,9 +77,9 @@ void ensureOwner_doesNotThrow_whenUserIsOwner() { } @Test - void ensureOwner_throwsSecurity_whenUserIsNotOwner() { + void ensureOwner_throwsForbidden_whenUserIsNotOwner() { assertThatThrownBy(() -> policy.ensureOwner(lobby, 2L)) - .isInstanceOf(SecurityException.class) + .isInstanceOf(ForbiddenException.class) .hasMessageContaining("owner"); } diff --git a/backend/lined/src/test/java/io/backend/lined/lobby/service/LobbyServiceImplTest.java b/backend/lined/src/test/java/io/backend/lined/lobby/service/LobbyServiceImplTest.java index 2f4e7ac..d14e0da 100644 --- a/backend/lined/src/test/java/io/backend/lined/lobby/service/LobbyServiceImplTest.java +++ b/backend/lined/src/test/java/io/backend/lined/lobby/service/LobbyServiceImplTest.java @@ -7,6 +7,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.backend.lined.common.exception.BadRequestException; +import io.backend.lined.common.exception.ForbiddenException; +import io.backend.lined.common.exception.NotFoundException; import io.backend.lined.lobby.api.LobbyCreateDto; import io.backend.lined.lobby.api.LobbyDto; import io.backend.lined.lobby.api.LobbyMapper; @@ -17,7 +20,6 @@ import io.backend.lined.user.domain.UserRepository; import java.util.HashSet; import java.util.List; -import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.BeforeEach; @@ -105,13 +107,13 @@ void create_ownerIsAddedAsMember() { } @Test - void create_throwsNoSuchElement_whenOwnerNotFound() { + void create_throwsNotFound_whenOwnerNotFound() { LobbyCreateDto dto = new LobbyCreateDto("Our Family", LobbyTypes.FAMILY); when(userRepo.findById(99L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> lobbyService.create(dto, 99L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("99"); verify(lobbyRepo, never()).save(any()); @@ -132,11 +134,11 @@ void getById_success() { } @Test - void getById_throwsNoSuchElement_whenLobbyNotFound() { + void getById_throwsNotFound_whenLobbyNotFound() { when(lobbyRepo.findById(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> lobbyService.getById(999L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("999"); } @@ -180,30 +182,30 @@ void addMember_success() { } @Test - void addMember_throwsNoSuchElement_whenLobbyNotFound() { + void addMember_throwsNotFound_whenLobbyNotFound() { when(lobbyRepo.findById(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> lobbyService.addMember(999L, 2L, 1L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("999"); } @Test - void addMember_throwsNoSuchElement_whenUserNotFound() { + void addMember_throwsNotFound_whenUserNotFound() { when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobbyEntity)); when(userRepo.findById(99L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> lobbyService.addMember(101L, 99L, 1L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("99"); } @Test - void addMember_throwsSecurityException_whenRequesterIsNotOwner() { + void addMember_throwsForbidden_whenRequesterIsNotOwner() { when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobbyEntity)); assertThatThrownBy(() -> lobbyService.addMember(101L, 2L, 99L)) - .isInstanceOf(SecurityException.class) + .isInstanceOf(ForbiddenException.class) .hasMessageContaining("owner"); } @@ -225,29 +227,29 @@ void removeMember_success() { } @Test - void removeMember_throwsIllegalArgument_whenRemovingOwner() { + void removeMember_throwsBadRequest_whenRemovingOwner() { when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobbyEntity)); assertThatThrownBy(() -> lobbyService.removeMember(101L, 1L, 1L)) - .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(BadRequestException.class) .hasMessageContaining("Owner cannot be removed"); } @Test - void removeMember_throwsNoSuchElement_whenLobbyNotFound() { + void removeMember_throwsNotFound_whenLobbyNotFound() { when(lobbyRepo.findById(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> lobbyService.removeMember(999L, 2L, 1L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("999"); } @Test - void removeMember_throwsSecurityException_whenRequesterIsNotOwner() { + void removeMember_throwsForbidden_whenRequesterIsNotOwner() { when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobbyEntity)); assertThatThrownBy(() -> lobbyService.removeMember(101L, 2L, 99L)) - .isInstanceOf(SecurityException.class) + .isInstanceOf(ForbiddenException.class) .hasMessageContaining("owner"); } @@ -265,24 +267,24 @@ void delete_success() { } @Test - void delete_throwsNoSuchElement_whenLobbyNotFound() { + void delete_throwsNotFound_whenLobbyNotFound() { when(lobbyRepo.findById(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> lobbyService.delete(999L, 1L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("999"); verify(lobbyRepo, never()).delete(any()); } @Test - void delete_throwsSecurityException_whenRequesterIsNotOwner() { + void delete_throwsForbidden_whenRequesterIsNotOwner() { when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobbyEntity)); assertThatThrownBy(() -> lobbyService.delete(101L, 99L)) - .isInstanceOf(SecurityException.class) + .isInstanceOf(ForbiddenException.class) .hasMessageContaining("owner"); verify(lobbyRepo, never()).delete(any()); } -} \ No newline at end of file +} diff --git a/backend/lined/src/test/java/io/backend/lined/task/service/TaskServiceImplTest.java b/backend/lined/src/test/java/io/backend/lined/task/service/TaskServiceImplTest.java index 383c710..308307b 100644 --- a/backend/lined/src/test/java/io/backend/lined/task/service/TaskServiceImplTest.java +++ b/backend/lined/src/test/java/io/backend/lined/task/service/TaskServiceImplTest.java @@ -7,6 +7,9 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import io.backend.lined.common.exception.BadRequestException; +import io.backend.lined.common.exception.ForbiddenException; +import io.backend.lined.common.exception.NotFoundException; import io.backend.lined.lobby.domain.LobbyEntity; import io.backend.lined.lobby.domain.LobbyRepository; import io.backend.lined.lobby.domain.LobbyTypes; @@ -22,7 +25,6 @@ import io.backend.lined.user.domain.UserRepository; import java.util.HashSet; import java.util.List; -import java.util.NoSuchElementException; import java.util.Optional; import java.util.Set; import org.junit.jupiter.api.BeforeEach; @@ -100,41 +102,41 @@ void create_success() { } @Test - void create_throwsNoSuchElement_whenCreatorNotFound() { + void create_throwsNotFound_whenCreatorNotFound() { TaskCreateDto dto = new TaskCreateDto("Buy groceries", 101L, null, null); when(userRepo.findById(99L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> taskService.create(dto, 99L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("99"); verify(repo, never()).save(any()); } @Test - void create_throwsNoSuchElement_whenLobbyNotFound() { + void create_throwsNotFound_whenLobbyNotFound() { TaskCreateDto dto = new TaskCreateDto("Buy groceries", 999L, null, null); when(userRepo.findById(1L)).thenReturn(Optional.of(owner)); when(lobbyRepo.findById(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> taskService.create(dto, 1L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("999"); verify(repo, never()).save(any()); } @Test - void create_throwsSecurity_whenUserIsNotLobbyMember() { + void create_throwsForbidden_whenUserIsNotLobbyMember() { TaskCreateDto dto = new TaskCreateDto("Buy groceries", 101L, null, null); when(userRepo.findById(99L)).thenReturn(Optional.of(new UserEntity())); when(lobbyRepo.findById(101L)).thenReturn(Optional.of(lobby)); assertThatThrownBy(() -> taskService.create(dto, 99L)) - .isInstanceOf(SecurityException.class) + .isInstanceOf(ForbiddenException.class) .hasMessageContaining("not a member"); verify(repo, never()).save(any()); @@ -173,24 +175,24 @@ void update_skipsNullFields() { } @Test - void update_throwsNoSuchElement_whenTaskNotFound() { + void update_throwsNotFound_whenTaskNotFound() { TaskUpdateDto dto = new TaskUpdateDto(null, null, null, "Title"); when(repo.findById(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> taskService.update(999L, dto, 1L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("999"); } @Test - void update_throwsSecurity_whenUserIsNotLobbyMember() { + void update_throwsForbidden_whenUserIsNotLobbyMember() { TaskUpdateDto dto = new TaskUpdateDto(null, null, null, "Title"); when(repo.findById(555L)).thenReturn(Optional.of(taskEntity)); assertThatThrownBy(() -> taskService.update(555L, dto, 99L)) - .isInstanceOf(SecurityException.class) + .isInstanceOf(ForbiddenException.class) .hasMessageContaining("not a member"); } @@ -209,22 +211,22 @@ void delete_success() { } @Test - void delete_throwsNoSuchElement_whenTaskNotFound() { + void delete_throwsNotFound_whenTaskNotFound() { when(repo.findById(999L)).thenReturn(Optional.empty()); assertThatThrownBy(() -> taskService.delete(999L, 1L)) - .isInstanceOf(NoSuchElementException.class) + .isInstanceOf(NotFoundException.class) .hasMessageContaining("999"); verify(repo, never()).delete(any(TaskEntity.class)); } @Test - void delete_throwsSecurity_whenUserIsNotLobbyMember() { + void delete_throwsForbidden_whenUserIsNotLobbyMember() { when(repo.findById(555L)).thenReturn(Optional.of(taskEntity)); assertThatThrownBy(() -> taskService.delete(555L, 99L)) - .isInstanceOf(SecurityException.class) + .isInstanceOf(ForbiddenException.class) .hasMessageContaining("not a member"); verify(repo, never()).delete(any(TaskEntity.class)); @@ -255,4 +257,11 @@ void list_returnsEmpty_whenNoTasksMatch() { assertThat(result).isEmpty(); } + @Test + void list_throwsBadRequest_whenStatusIsInvalid() { + assertThatThrownBy(() -> taskService.list(null, null, "INVALID_STATUS")) + .isInstanceOf(BadRequestException.class) + .hasMessageContaining("Invalid status value"); + } + }