From f0bd1eb4d11095007959c53dc788e0b7d5d56feb Mon Sep 17 00:00:00 2001 From: Oleksii Makieiev Date: Mon, 1 Jun 2026 00:57:37 +0300 Subject: [PATCH 1/3] Add BadRequestException and ForbiddenException; handle FORBIDDEN in GlobalExceptionHandler Introduces two new application exception types extending BaseAppException so 400 and 403 responses flow through the ProblemDetail seam instead of leaking raw Java exceptions. Adds an explicit FORBIDDEN title case to the handler switch for consistency with the other mapped statuses. Co-Authored-By: Claude Sonnet 4.6 --- .../lined/common/exception/BadRequestException.java | 10 ++++++++++ .../lined/common/exception/ForbiddenException.java | 10 ++++++++++ .../backend/lined/config/GlobalExceptionHandler.java | 1 + 3 files changed, 21 insertions(+) create mode 100644 backend/lined/src/main/java/io/backend/lined/common/exception/BadRequestException.java create mode 100644 backend/lined/src/main/java/io/backend/lined/common/exception/ForbiddenException.java 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())); From b55781113f09b1874f15dbf98a42beb55f2704f8 Mon Sep 17 00:00:00 2001 From: Oleksii Makieiev Date: Mon, 1 Jun 2026 00:57:48 +0300 Subject: [PATCH 2/3] Replace raw exceptions in services and LobbyAccessPolicy with application exceptions - LobbyAccessPolicy: SecurityException -> ForbiddenException in ensureMember/ensureOwner; add Objects.requireNonNull(lobby) guard in both methods - EventServiceImpl: NoSuchElementException -> NotFoundException via EntityFinder.findOrThrow in all must* helpers; IllegalArgumentException -> BadRequestException for date-range checks - TaskServiceImpl: same NotFoundException pattern; wrap TaskStatus.valueOf with BadRequestException to prevent HTTP 500 on invalid status strings - LobbyServiceImpl: NoSuchElementException -> NotFoundException; IllegalArgumentException -> BadRequestException for owner-removal guard - EventEntity: remove IllegalArgumentException from @PrePersist; service layer validates startAt < endAt before save, making the entity-level check both duplicate and a 500 risk - EventService/TaskService interfaces: update @throws Javadoc to reflect actual exception types Co-Authored-By: Claude Sonnet 4.6 --- .../lined/event/domain/EventEntity.java | 3 -- .../lined/event/service/EventService.java | 29 ++++++++++--------- .../lined/event/service/EventServiceImpl.java | 26 +++++++++-------- .../lobby/service/LobbyAccessPolicy.java | 7 +++-- .../lined/lobby/service/LobbyServiceImpl.java | 15 +++++----- .../lined/task/service/TaskService.java | 19 +++++++----- .../lined/task/service/TaskServiceImpl.java | 20 ++++++++----- 7 files changed, 67 insertions(+), 52 deletions(-) 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))); } } From a73f0388572c2004bdff40259693b7c895308fa9 Mon Sep 17 00:00:00 2001 From: Oleksii Makieiev Date: Mon, 1 Jun 2026 00:57:56 +0300 Subject: [PATCH 3/3] Update tests: assert application exceptions, no raw Java exceptions Replace all NoSuchElementException -> NotFoundException, IllegalArgumentException -> BadRequestException, SecurityException -> ForbiddenException assertions across all affected service and policy test files. Rename test methods accordingly (e.g. throwsNoSuchElement_ -> throwsNotFound_) for clarity. Add list_throwsBadRequest_whenStatusIsInvalid to cover the TaskStatus.valueOf guard. Remove all imports of NoSuchElementException from test files. Co-Authored-By: Claude Sonnet 4.6 --- .../service/EventServiceImplConflictTest.java | 26 +++---- .../event/service/EventServiceImplTest.java | 68 ++++++++++--------- .../lobby/service/LobbyAccessPolicyTest.java | 9 +-- .../lobby/service/LobbyServiceImplTest.java | 46 +++++++------ .../task/service/TaskServiceImplTest.java | 39 +++++++---- 5 files changed, 102 insertions(+), 86 deletions(-) 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"); + } + }