diff --git a/src/java/src/main/java/org/openapitools/controller/LampsController.java b/src/java/src/main/java/org/openapitools/controller/LampsController.java index c8fb190a..8ae2bcc6 100644 --- a/src/java/src/main/java/org/openapitools/controller/LampsController.java +++ b/src/java/src/main/java/org/openapitools/controller/LampsController.java @@ -6,7 +6,7 @@ import java.util.concurrent.CompletableFuture; import lombok.RequiredArgsConstructor; import org.openapitools.api.LampsApi; -import org.openapitools.model.Error; +import org.openapitools.exception.LampNotFoundException; import org.openapitools.model.Lamp; import org.openapitools.model.LampCreate; import org.openapitools.model.LampUpdate; @@ -32,49 +32,31 @@ public CompletableFuture> createLamp(final LampCreate lampC lamp.setStatus(lampCreate.getStatus()); final Lamp created = lampService.create(lamp); return ResponseEntity.status(HttpStatus.CREATED).body(created); - }); + }, + Runnable::run); } @Override - @SuppressWarnings("unchecked") public CompletableFuture> deleteLamp(final String lampId) { return CompletableFuture.supplyAsync( () -> { - try { - final UUID lampUuid = UUID.fromString(lampId); - final boolean deleted = lampService.delete(lampUuid); - if (deleted) { - return ResponseEntity.noContent().build(); - } else { - return ResponseEntity.notFound().build(); - } - } catch (IllegalArgumentException e) { - final Error error = new Error("INVALID_ARGUMENT"); - return (ResponseEntity) - (ResponseEntity) ResponseEntity.badRequest().body(error); - } - }); + final UUID lampUuid = UUID.fromString(lampId); + lampService.delete(lampUuid); + return ResponseEntity.noContent().build(); + }, + Runnable::run); } @Override - @SuppressWarnings("unchecked") public CompletableFuture> getLamp(final String lampId) { return CompletableFuture.supplyAsync( () -> { - try { - final UUID lampUuid = UUID.fromString(lampId); - final Lamp lamp = lampService.findById(lampUuid); - if (lamp != null) { - return ResponseEntity.ok().body(lamp); - } else { - return ResponseEntity.notFound().build(); - } - } catch (IllegalArgumentException e) { - final Error error = new Error("INVALID_ARGUMENT"); - return (ResponseEntity) - (ResponseEntity) ResponseEntity.badRequest().body(error); - } - }); + final UUID lampUuid = UUID.fromString(lampId); + final Lamp lamp = + lampService.findById(lampUuid).orElseThrow(() -> new LampNotFoundException(lampUuid)); + return ResponseEntity.ok().body(lamp); + }, + Runnable::run); } @Override @@ -87,30 +69,21 @@ public CompletableFuture> listLamps( response.setData(lamps); response.setHasMore(false); return ResponseEntity.ok().body(response); - }); + }, + Runnable::run); } @Override - @SuppressWarnings("unchecked") public CompletableFuture> updateLamp( final String lampId, final LampUpdate lampUpdate) { return CompletableFuture.supplyAsync( () -> { - try { - final UUID lampUuid = UUID.fromString(lampId); - final Lamp lampData = new Lamp(); - lampData.setStatus(lampUpdate.getStatus()); - final Lamp updated = lampService.update(lampUuid, lampData); - if (updated != null) { - return ResponseEntity.ok().body(updated); - } else { - return ResponseEntity.notFound().build(); - } - } catch (IllegalArgumentException e) { - final Error error = new Error("INVALID_ARGUMENT"); - return (ResponseEntity) - (ResponseEntity) ResponseEntity.badRequest().body(error); - } - }); + final UUID lampUuid = UUID.fromString(lampId); + final Lamp lampData = new Lamp(); + lampData.setStatus(lampUpdate.getStatus()); + final Lamp updated = lampService.update(lampUuid, lampData); + return ResponseEntity.ok().body(updated); + }, + Runnable::run); } } diff --git a/src/java/src/main/java/org/openapitools/exception/GlobalExceptionHandler.java b/src/java/src/main/java/org/openapitools/exception/GlobalExceptionHandler.java index 8e3ac916..5f3549bf 100644 --- a/src/java/src/main/java/org/openapitools/exception/GlobalExceptionHandler.java +++ b/src/java/src/main/java/org/openapitools/exception/GlobalExceptionHandler.java @@ -70,6 +70,17 @@ public ResponseEntity handleNullPointerException(final NullPointerExcepti return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); } + /** + * Handle lamp not found exceptions. + * + * @param ex the lamp not found exception + * @return 404 Not Found + */ + @ExceptionHandler(LampNotFoundException.class) + public ResponseEntity handleLampNotFoundException(final LampNotFoundException ex) { + return ResponseEntity.notFound().build(); + } + /** * Handle illegal argument exceptions (e.g., invalid UUID format). * diff --git a/src/java/src/main/java/org/openapitools/exception/LampNotFoundException.java b/src/java/src/main/java/org/openapitools/exception/LampNotFoundException.java new file mode 100644 index 00000000..bcc60210 --- /dev/null +++ b/src/java/src/main/java/org/openapitools/exception/LampNotFoundException.java @@ -0,0 +1,24 @@ +package org.openapitools.exception; + +import java.io.Serial; +import java.util.UUID; + +/** + * Exception thrown when a lamp with a given ID cannot be found. This is a domain-level exception + * that maps to HTTP 404 responses. + */ +public class LampNotFoundException extends RuntimeException { + + @Serial private static final long serialVersionUID = 1L; + + private final UUID lampId; + + public LampNotFoundException(final UUID lampId) { + super("Lamp not found: " + lampId); + this.lampId = lampId; + } + + public UUID getLampId() { + return lampId; + } +} diff --git a/src/java/src/main/java/org/openapitools/repository/impl/InMemoryLampRepository.java b/src/java/src/main/java/org/openapitools/repository/impl/InMemoryLampRepository.java index e4a4e88e..6b316cf2 100644 --- a/src/java/src/main/java/org/openapitools/repository/impl/InMemoryLampRepository.java +++ b/src/java/src/main/java/org/openapitools/repository/impl/InMemoryLampRepository.java @@ -7,7 +7,6 @@ import java.util.Optional; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; import org.openapitools.config.OnNoDatabaseUrlCondition; import org.openapitools.entity.LampEntity; import org.openapitools.repository.LampRepository; @@ -129,7 +128,7 @@ public List findByStatus(final Boolean isOn) { return lamps.values().stream() .filter(lamp -> lamp.getDeletedAt() == null) .filter(lamp -> lamp.getStatus().equals(isOn)) - .collect(Collectors.toList()); + .toList(); } @Override @@ -137,7 +136,7 @@ public List findAllActive() { return lamps.values().stream() .filter(lamp -> lamp.getDeletedAt() == null) .sorted(Comparator.comparing(LampEntity::getCreatedAt)) - .collect(Collectors.toList()); + .toList(); } @Override diff --git a/src/java/src/main/java/org/openapitools/service/LampService.java b/src/java/src/main/java/org/openapitools/service/LampService.java index 338ccb51..fa1cce1b 100644 --- a/src/java/src/main/java/org/openapitools/service/LampService.java +++ b/src/java/src/main/java/org/openapitools/service/LampService.java @@ -2,10 +2,11 @@ import java.time.OffsetDateTime; import java.util.List; +import java.util.Optional; import java.util.UUID; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.openapitools.entity.LampEntity; +import org.openapitools.exception.LampNotFoundException; import org.openapitools.mapper.LampMapper; import org.openapitools.model.Lamp; import org.openapitools.repository.LampRepository; @@ -57,10 +58,10 @@ public Lamp create(final Lamp lamp) { * Find a lamp by its ID. * * @param id the lamp ID - * @return the lamp if found, null otherwise + * @return optional containing the lamp if found, empty otherwise */ - public Lamp findById(final UUID id) { - return repository.findById(id).map(mapper::toModel).orElse(null); + public Optional findById(final UUID id) { + return repository.findById(id).map(mapper::toModel); } /** @@ -75,7 +76,7 @@ public List findAll(final int offset, final int limit) { PageRequest.of(offset / limit, limit, Sort.by(Sort.Direction.ASC, "createdAt")); final Page page = repository.findAll(pageable); - return page.getContent().stream().map(mapper::toModel).collect(Collectors.toList()); + return page.getContent().stream().map(mapper::toModel).toList(); } /** @@ -84,7 +85,7 @@ public List findAll(final int offset, final int limit) { * @return list of all active lamps ordered by creation time */ public List findAllActive() { - return repository.findAllActive().stream().map(mapper::toModel).collect(Collectors.toList()); + return repository.findAllActive().stream().map(mapper::toModel).toList(); } /** @@ -94,7 +95,7 @@ public List findAllActive() { * @return list of lamps with the specified status */ public List findByStatus(final Boolean isOn) { - return repository.findByStatus(isOn).stream().map(mapper::toModel).collect(Collectors.toList()); + return repository.findByStatus(isOn).stream().map(mapper::toModel).toList(); } /** @@ -111,7 +112,8 @@ public long countActive() { * * @param id the lamp ID * @param lamp the updated lamp data - * @return the updated lamp if found, null otherwise + * @return the updated lamp + * @throws LampNotFoundException if no lamp exists with the given ID */ @Transactional public Lamp update(final UUID id, final Lamp lamp) { @@ -123,7 +125,7 @@ public Lamp update(final UUID id, final Lamp lamp) { // updatedAt is automatically set by @UpdateTimestamp return mapper.toModel(repository.save(entity)); }) - .orElse(null); + .orElseThrow(() -> new LampNotFoundException(id)); } /** @@ -133,18 +135,13 @@ public Lamp update(final UUID id, final Lamp lamp) { * LampEntity. * * @param id the lamp ID to delete - * @return true if the lamp was found and deleted, false otherwise + * @throws LampNotFoundException if no lamp exists with the given ID */ @Transactional - public boolean delete(final UUID id) { - return repository - .findById(id) - .map( - entity -> { - entity.setDeletedAt(OffsetDateTime.now()); - repository.save(entity); - return true; - }) - .orElse(false); + public void delete(final UUID id) { + final LampEntity entity = + repository.findById(id).orElseThrow(() -> new LampNotFoundException(id)); + entity.setDeletedAt(OffsetDateTime.now()); + repository.save(entity); } } diff --git a/src/java/src/test/java/org/openapitools/controller/LampsControllerTest.java b/src/java/src/test/java/org/openapitools/controller/LampsControllerTest.java index 7016545b..a334aa24 100644 --- a/src/java/src/test/java/org/openapitools/controller/LampsControllerTest.java +++ b/src/java/src/test/java/org/openapitools/controller/LampsControllerTest.java @@ -1,6 +1,7 @@ package org.openapitools.controller; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -8,9 +9,11 @@ import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.openapitools.exception.LampNotFoundException; import org.openapitools.model.Lamp; import org.openapitools.model.LampCreate; import org.openapitools.model.LampUpdate; @@ -65,7 +68,7 @@ void listLamps_ShouldReturnAllLamps() throws Exception { @Test void getLamp_WithValidId_ShouldReturnLamp() throws Exception { // Given - when(lampService.findById(testLampId)).thenReturn(testLamp); + when(lampService.findById(testLampId)).thenReturn(Optional.of(testLamp)); // When & Then MvcResult result = @@ -85,7 +88,7 @@ void getLamp_WithValidId_ShouldReturnLamp() throws Exception { @Test void getLamp_WithNonExistentId_ShouldReturn404() throws Exception { // Given - when(lampService.findById(testLampId)).thenReturn(null); + when(lampService.findById(testLampId)).thenReturn(Optional.empty()); // When & Then MvcResult result = @@ -172,7 +175,8 @@ void updateLamp_WithNonExistentId_ShouldReturn404() throws Exception { LampUpdate lampUpdate = new LampUpdate(); lampUpdate.setStatus(false); - when(lampService.update(any(UUID.class), any(Lamp.class))).thenReturn(null); + when(lampService.update(any(UUID.class), any(Lamp.class))) + .thenThrow(new LampNotFoundException(testLampId)); // When & Then MvcResult result = @@ -190,8 +194,7 @@ void updateLamp_WithNonExistentId_ShouldReturn404() throws Exception { @Test void deleteLamp_WithValidId_ShouldDelete() throws Exception { - // Given - when(lampService.delete(testLampId)).thenReturn(true); + // Given — delete is void, no mock setup needed // When & Then MvcResult result = @@ -206,7 +209,7 @@ void deleteLamp_WithValidId_ShouldDelete() throws Exception { @Test void deleteLamp_WithNonExistentId_ShouldReturn404() throws Exception { // Given - when(lampService.delete(testLampId)).thenReturn(false); + doThrow(new LampNotFoundException(testLampId)).when(lampService).delete(testLampId); // When & Then MvcResult result = diff --git a/src/java/src/test/java/org/openapitools/exception/GlobalExceptionHandlerUnitTest.java b/src/java/src/test/java/org/openapitools/exception/GlobalExceptionHandlerUnitTest.java index d4428e2a..5c67bc4d 100644 --- a/src/java/src/test/java/org/openapitools/exception/GlobalExceptionHandlerUnitTest.java +++ b/src/java/src/test/java/org/openapitools/exception/GlobalExceptionHandlerUnitTest.java @@ -2,6 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; +import java.util.UUID; import org.junit.jupiter.api.Test; import org.openapitools.model.Error; import org.springframework.http.HttpStatus; @@ -23,6 +24,16 @@ void handleNullPointerException_ShouldReturnBadRequest() { assertThat(response.getBody().getError()).isEqualTo("INVALID_ARGUMENT"); } + @Test + void handleLampNotFoundException_ShouldReturnNotFound() { + LampNotFoundException ex = new LampNotFoundException(UUID.randomUUID()); + + ResponseEntity response = handler.handleLampNotFoundException(ex); + + assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + assertThat(response.getBody()).isNull(); + } + @Test void handleIllegalArgumentException_ShouldReturnBadRequest() { IllegalArgumentException ex = new IllegalArgumentException("bad argument"); diff --git a/src/java/src/test/java/org/openapitools/exception/LampNotFoundExceptionTest.java b/src/java/src/test/java/org/openapitools/exception/LampNotFoundExceptionTest.java new file mode 100644 index 00000000..4fbd3a63 --- /dev/null +++ b/src/java/src/test/java/org/openapitools/exception/LampNotFoundExceptionTest.java @@ -0,0 +1,20 @@ +package org.openapitools.exception; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.UUID; +import org.junit.jupiter.api.Test; + +/** Unit tests for LampNotFoundException. */ +class LampNotFoundExceptionTest { + + @Test + void shouldContainLampIdInMessage() { + final UUID lampId = UUID.randomUUID(); + + final LampNotFoundException ex = new LampNotFoundException(lampId); + + assertThat(ex.getMessage()).contains(lampId.toString()); + assertThat(ex.getLampId()).isEqualTo(lampId); + } +} diff --git a/src/java/src/test/java/org/openapitools/service/LampServiceTest.java b/src/java/src/test/java/org/openapitools/service/LampServiceTest.java index 0baf0f94..be1fb3b7 100644 --- a/src/java/src/test/java/org/openapitools/service/LampServiceTest.java +++ b/src/java/src/test/java/org/openapitools/service/LampServiceTest.java @@ -1,6 +1,7 @@ package org.openapitools.service; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @@ -14,6 +15,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.openapitools.entity.LampEntity; +import org.openapitools.exception.LampNotFoundException; import org.openapitools.mapper.LampMapper; import org.openapitools.model.Lamp; import org.openapitools.repository.LampRepository; @@ -76,26 +78,26 @@ void shouldFindLampById() { when(mapper.toModel(testEntity)).thenReturn(testLamp); // Act - final Lamp result = service.findById(testId); + final Optional result = service.findById(testId); // Assert - assertThat(result).isNotNull(); - assertThat(result.getId()).isEqualTo(testId); + assertThat(result).isPresent(); + assertThat(result.get().getId()).isEqualTo(testId); verify(repository).findById(testId); verify(mapper).toModel(testEntity); } @Test - void shouldReturnNullWhenLampNotFound() { + void shouldReturnEmptyWhenLampNotFound() { // Arrange final UUID nonExistentId = UUID.randomUUID(); when(repository.findById(nonExistentId)).thenReturn(Optional.empty()); // Act - final Lamp result = service.findById(nonExistentId); + final Optional result = service.findById(nonExistentId); // Assert - assertThat(result).isNull(); + assertThat(result).isEmpty(); verify(repository).findById(nonExistentId); verify(mapper, never()).toModel(any()); } @@ -190,7 +192,7 @@ void shouldUpdateLamp() { } @Test - void shouldReturnNullWhenUpdatingNonExistentLamp() { + void shouldThrowWhenUpdatingNonExistentLamp() { // Arrange final UUID nonExistentId = UUID.randomUUID(); final Lamp updateData = new Lamp(); @@ -198,11 +200,9 @@ void shouldReturnNullWhenUpdatingNonExistentLamp() { when(repository.findById(nonExistentId)).thenReturn(Optional.empty()); - // Act - final Lamp result = service.update(nonExistentId, updateData); - - // Assert - assertThat(result).isNull(); + // Act & Assert + assertThatThrownBy(() -> service.update(nonExistentId, updateData)) + .isInstanceOf(LampNotFoundException.class); verify(repository).findById(nonExistentId); verify(repository, never()).save(any()); } @@ -214,25 +214,22 @@ void shouldSoftDeleteLamp() { when(repository.save(testEntity)).thenReturn(testEntity); // Act - final boolean result = service.delete(testId); + service.delete(testId); // Assert - assertThat(result).isTrue(); verify(repository).findById(testId); verify(repository).save(any(LampEntity.class)); } @Test - void shouldReturnFalseWhenDeletingNonExistentLamp() { + void shouldThrowWhenDeletingNonExistentLamp() { // Arrange final UUID nonExistentId = UUID.randomUUID(); when(repository.findById(nonExistentId)).thenReturn(Optional.empty()); - // Act - final boolean result = service.delete(nonExistentId); - - // Assert - assertThat(result).isFalse(); + // Act & Assert + assertThatThrownBy(() -> service.delete(nonExistentId)) + .isInstanceOf(LampNotFoundException.class); verify(repository).findById(nonExistentId); verify(repository, never()).save(any()); } diff --git a/src/java/target/site/jacoco/jacoco.xml b/src/java/target/site/jacoco/jacoco.xml index 6ea5a489..d463a0e0 100644 --- a/src/java/target/site/jacoco/jacoco.xml +++ b/src/java/target/site/jacoco/jacoco.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file