diff --git a/src/main/java/com/onboarding/camera/cameraonboarding/controller/CameraRestController.java b/src/main/java/com/onboarding/camera/cameraonboarding/controller/CameraRestController.java index 6ce1de5..9c47e52 100644 --- a/src/main/java/com/onboarding/camera/cameraonboarding/controller/CameraRestController.java +++ b/src/main/java/com/onboarding/camera/cameraonboarding/controller/CameraRestController.java @@ -1,9 +1,13 @@ package com.onboarding.camera.cameraonboarding.controller; import com.onboarding.camera.cameraonboarding.converter.CameraDtoConverter; +import com.onboarding.camera.cameraonboarding.converter.LocationDtoConverter; import com.onboarding.camera.cameraonboarding.dto.CameraDto; import com.onboarding.camera.cameraonboarding.dto.CameraResponse; +import com.onboarding.camera.cameraonboarding.dto.LocationDto; +import com.onboarding.camera.cameraonboarding.dto.LocationResponse; import com.onboarding.camera.cameraonboarding.entity.Camera; +import com.onboarding.camera.cameraonboarding.entity.Location; import com.onboarding.camera.cameraonboarding.service.CameraService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -32,6 +36,8 @@ public class CameraRestController { private final CameraDtoConverter cameraDtoConverter; + private final LocationDtoConverter locationDtoConverter; + @PostMapping("/onboard") public ResponseEntity saveCamera(@Valid @RequestBody CameraDto cameraDto) { @@ -79,4 +85,16 @@ public ResponseEntity downloadImage(@PathVariable UUID cameraId) { .header(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + camera.getImageId() + ".png") .body(imageData); } + + @PostMapping("/camera/{cameraId}/location") + public ResponseEntity addLocation( + @PathVariable UUID cameraId, + @Valid @RequestBody LocationDto locationDto) { + + Camera updatedCamera = cameraService.handleAddLocation(cameraId, locationDto); + Location cameraLocation = updatedCamera.getLocation(); + LocationResponse response = locationDtoConverter.toLocationResponse(cameraLocation); + + return new ResponseEntity<>(response, HttpStatus.CREATED); + } } diff --git a/src/main/java/com/onboarding/camera/cameraonboarding/controller/GlobalExceptionHandler.java b/src/main/java/com/onboarding/camera/cameraonboarding/controller/GlobalExceptionHandler.java index 936dfaf..30bd025 100644 --- a/src/main/java/com/onboarding/camera/cameraonboarding/controller/GlobalExceptionHandler.java +++ b/src/main/java/com/onboarding/camera/cameraonboarding/controller/GlobalExceptionHandler.java @@ -9,6 +9,7 @@ import com.onboarding.camera.cameraonboarding.exception.ImageNotDownloadedException; import com.onboarding.camera.cameraonboarding.exception.ImageNotFoundException; import com.onboarding.camera.cameraonboarding.exception.ImageNotUploadedException; +import com.onboarding.camera.cameraonboarding.exception.LocationNotAddedException; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; @@ -153,5 +154,17 @@ public ResponseEntity handleImageNotDownloadedException(ImageNotD return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); } + + @ExceptionHandler(LocationNotAddedException.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + @ResponseBody + public ResponseEntity handleLocationNotAddedException(LocationNotAddedException ex) { + ErrorResponse errorResponse = new ErrorResponse(); + + errorResponse.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); + errorResponse.setMessage(ex.getMessage()); + + return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR); + } } diff --git a/src/main/java/com/onboarding/camera/cameraonboarding/converter/LocationDtoConverter.java b/src/main/java/com/onboarding/camera/cameraonboarding/converter/LocationDtoConverter.java new file mode 100644 index 0000000..00f18d6 --- /dev/null +++ b/src/main/java/com/onboarding/camera/cameraonboarding/converter/LocationDtoConverter.java @@ -0,0 +1,17 @@ +package com.onboarding.camera.cameraonboarding.converter; + +import com.onboarding.camera.cameraonboarding.dto.LocationResponse; +import com.onboarding.camera.cameraonboarding.entity.Location; +import org.springframework.stereotype.Component; + +@Component +public class LocationDtoConverter { + + public LocationResponse toLocationResponse(Location location) { + LocationResponse response = new LocationResponse(); + response.setLatitude(location.getLatitude()); + response.setLongitude(location.getLongitude()); + response.setAddress(location.getAddress()); + return response; + } +} diff --git a/src/main/java/com/onboarding/camera/cameraonboarding/dto/LocationDto.java b/src/main/java/com/onboarding/camera/cameraonboarding/dto/LocationDto.java new file mode 100644 index 0000000..91e9fa6 --- /dev/null +++ b/src/main/java/com/onboarding/camera/cameraonboarding/dto/LocationDto.java @@ -0,0 +1,30 @@ +package com.onboarding.camera.cameraonboarding.dto; + +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class LocationDto { + + @NotNull(message = "Latitude cannot be null") + @DecimalMin(value = "-90.0", message = "Latitude must be >= -90") + @DecimalMax(value = "90.0", message = "Latitude must be <= 90") + private double latitude; + + @NotNull(message = "Longitude cannot be null") + @DecimalMin(value = "-180.0", message = "Longitude must be >= -180") + @DecimalMax(value = "180.0", message = "Longitude must be <= 180") + private double longitude; + + @NotBlank(message = "Address cannot be blank") + @Size(min = 10, max = 100, message = "Address should be up to 100 characters") + private String address; +} diff --git a/src/main/java/com/onboarding/camera/cameraonboarding/dto/LocationResponse.java b/src/main/java/com/onboarding/camera/cameraonboarding/dto/LocationResponse.java new file mode 100644 index 0000000..97de16a --- /dev/null +++ b/src/main/java/com/onboarding/camera/cameraonboarding/dto/LocationResponse.java @@ -0,0 +1,10 @@ +package com.onboarding.camera.cameraonboarding.dto; + +import lombok.Data; + +@Data +public class LocationResponse { + private double latitude; + private double longitude; + private String address; +} diff --git a/src/main/java/com/onboarding/camera/cameraonboarding/entity/Camera.java b/src/main/java/com/onboarding/camera/cameraonboarding/entity/Camera.java index dc4f30d..389f5df 100644 --- a/src/main/java/com/onboarding/camera/cameraonboarding/entity/Camera.java +++ b/src/main/java/com/onboarding/camera/cameraonboarding/entity/Camera.java @@ -1,9 +1,12 @@ package com.onboarding.camera.cameraonboarding.entity; +import com.fasterxml.jackson.annotation.JsonManagedReference; +import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; import jakarta.persistence.Table; import lombok.Data; import lombok.ToString; @@ -13,7 +16,7 @@ import java.util.UUID; @Data -@ToString +@ToString(exclude = "location") @Entity @Table(name = "camera_metadata") public class Camera { @@ -47,4 +50,8 @@ public class Camera { @Column(name = "initialized_at") private LocalDateTime initializedAt; + + @OneToOne(mappedBy = "camera", cascade = CascadeType.ALL, orphanRemoval = true) + @JsonManagedReference + private Location location; } diff --git a/src/main/java/com/onboarding/camera/cameraonboarding/entity/Location.java b/src/main/java/com/onboarding/camera/cameraonboarding/entity/Location.java new file mode 100644 index 0000000..52e72fc --- /dev/null +++ b/src/main/java/com/onboarding/camera/cameraonboarding/entity/Location.java @@ -0,0 +1,42 @@ +package com.onboarding.camera.cameraonboarding.entity; + +import com.fasterxml.jackson.annotation.JsonBackReference; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.ToString; +import org.hibernate.annotations.UuidGenerator; + +import java.util.UUID; + +@Data +@ToString(exclude = "camera") +@Entity +@Table(name = "location") +public class Location { + + @Id + @GeneratedValue + @UuidGenerator + @Column(name = "location_id") + private UUID locationId; + + @Column(name = "latitude", nullable = false) + private Double latitude; + + @Column(name = "longitude", nullable = false) + private Double longitude; + + @Column(name = "address", nullable = false) + private String address; + + @OneToOne + @JoinColumn(name = "camera_id", referencedColumnName = "cam_id") + @JsonBackReference + private Camera camera; +} diff --git a/src/main/java/com/onboarding/camera/cameraonboarding/exception/LocationNotAddedException.java b/src/main/java/com/onboarding/camera/cameraonboarding/exception/LocationNotAddedException.java new file mode 100644 index 0000000..38b79c7 --- /dev/null +++ b/src/main/java/com/onboarding/camera/cameraonboarding/exception/LocationNotAddedException.java @@ -0,0 +1,7 @@ +package com.onboarding.camera.cameraonboarding.exception; + +public class LocationNotAddedException extends RuntimeException { + public LocationNotAddedException(String message) { + super(message); + } +} diff --git a/src/main/java/com/onboarding/camera/cameraonboarding/service/CameraService.java b/src/main/java/com/onboarding/camera/cameraonboarding/service/CameraService.java index bce445f..e706b27 100644 --- a/src/main/java/com/onboarding/camera/cameraonboarding/service/CameraService.java +++ b/src/main/java/com/onboarding/camera/cameraonboarding/service/CameraService.java @@ -1,5 +1,6 @@ package com.onboarding.camera.cameraonboarding.service; +import com.onboarding.camera.cameraonboarding.dto.LocationDto; import com.onboarding.camera.cameraonboarding.entity.Camera; import com.onboarding.camera.cameraonboarding.exception.CameraAlreadyInitializedException; import com.onboarding.camera.cameraonboarding.exception.CameraNotCreatedException; @@ -8,6 +9,7 @@ import com.onboarding.camera.cameraonboarding.exception.ImageNotDownloadedException; import com.onboarding.camera.cameraonboarding.exception.ImageNotFoundException; import com.onboarding.camera.cameraonboarding.exception.ImageNotUploadedException; +import com.onboarding.camera.cameraonboarding.exception.LocationNotAddedException; import java.util.UUID; @@ -70,4 +72,16 @@ public interface CameraService { */ byte[] handleDownloadImage(UUID cameraId); + + /** + * this method is used for add location information to camera + * + * @param cameraId camera id + * @param locationDto camera location + * @return updated camera + * @throws CameraNotFoundException if camera is not found with id + * @throws LocationNotAddedException if unexpected error occurs while adding location + */ + + Camera handleAddLocation(UUID cameraId, LocationDto locationDto); } diff --git a/src/main/java/com/onboarding/camera/cameraonboarding/service/impl/CameraServiceImpl.java b/src/main/java/com/onboarding/camera/cameraonboarding/service/impl/CameraServiceImpl.java index 0418f91..2da41a4 100644 --- a/src/main/java/com/onboarding/camera/cameraonboarding/service/impl/CameraServiceImpl.java +++ b/src/main/java/com/onboarding/camera/cameraonboarding/service/impl/CameraServiceImpl.java @@ -1,6 +1,8 @@ package com.onboarding.camera.cameraonboarding.service.impl; +import com.onboarding.camera.cameraonboarding.dto.LocationDto; import com.onboarding.camera.cameraonboarding.entity.Camera; +import com.onboarding.camera.cameraonboarding.entity.Location; import com.onboarding.camera.cameraonboarding.exception.CameraAlreadyInitializedException; import com.onboarding.camera.cameraonboarding.exception.CameraNotCreatedException; import com.onboarding.camera.cameraonboarding.exception.CameraNotFoundException; @@ -9,6 +11,7 @@ import com.onboarding.camera.cameraonboarding.exception.ImageNotDownloadedException; import com.onboarding.camera.cameraonboarding.exception.ImageNotFoundException; import com.onboarding.camera.cameraonboarding.exception.ImageNotUploadedException; +import com.onboarding.camera.cameraonboarding.exception.LocationNotAddedException; import com.onboarding.camera.cameraonboarding.repository.CameraRepository; import com.onboarding.camera.cameraonboarding.service.BlobStorageService; import com.onboarding.camera.cameraonboarding.service.CameraService; @@ -16,8 +19,10 @@ import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import java.io.ByteArrayOutputStream; +import java.util.Optional; import java.util.UUID; @Slf4j @@ -120,6 +125,29 @@ public byte[] handleDownloadImage(UUID cameraId) { } } + @Override + @Transactional + public Camera handleAddLocation(UUID cameraId, LocationDto locationDto) { + Camera camera = getCameraById(cameraId); + + try { + Location location = Optional.ofNullable(camera.getLocation()).orElse(new Location()); + location.setLatitude(locationDto.getLatitude()); + location.setLongitude(locationDto.getLongitude()); + location.setAddress(locationDto.getAddress()); + + location.setCamera(camera); + camera.setLocation(location); + + cameraRepository.save(camera); + log.info("Location added/updated successfully for Camera ID: {}", cameraId); + return camera; + } catch (Exception ex) { + log.error("Exception occurred while adding location, camera:{}:ex:{}", cameraId, ex.getMessage()); + throw new LocationNotAddedException(String.format("Error occurred while adding location: %s", ex.getMessage())); + } + } + /** * Validates if the camera has been onboarded and initialized * diff --git a/src/main/resources/db/changelog/changelog-master.xml b/src/main/resources/db/changelog/changelog-master.xml index 651cba4..fa0bdc8 100644 --- a/src/main/resources/db/changelog/changelog-master.xml +++ b/src/main/resources/db/changelog/changelog-master.xml @@ -3,8 +3,9 @@ xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog - http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> + https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd"> + \ No newline at end of file diff --git a/src/main/resources/db/changelog/changelog-v2.xml b/src/main/resources/db/changelog/changelog-v2.xml new file mode 100644 index 0000000..1da15d0 --- /dev/null +++ b/src/main/resources/db/changelog/changelog-v2.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/com/onboarding/camera/cameraonboarding/controller/CameraRestControllerTest.java b/src/test/java/com/onboarding/camera/cameraonboarding/controller/CameraRestControllerTest.java index 19a1c40..e1fc176 100644 --- a/src/test/java/com/onboarding/camera/cameraonboarding/controller/CameraRestControllerTest.java +++ b/src/test/java/com/onboarding/camera/cameraonboarding/controller/CameraRestControllerTest.java @@ -2,12 +2,16 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.onboarding.camera.cameraonboarding.converter.CameraDtoConverter; +import com.onboarding.camera.cameraonboarding.converter.LocationDtoConverter; import com.onboarding.camera.cameraonboarding.dto.CameraDto; +import com.onboarding.camera.cameraonboarding.dto.LocationDto; import com.onboarding.camera.cameraonboarding.entity.Camera; +import com.onboarding.camera.cameraonboarding.entity.Location; import com.onboarding.camera.cameraonboarding.exception.CameraAlreadyInitializedException; import com.onboarding.camera.cameraonboarding.exception.CameraNotFoundException; import com.onboarding.camera.cameraonboarding.exception.CameraNotInitializedException; import com.onboarding.camera.cameraonboarding.exception.ImageAlreadyUploadedException; +import com.onboarding.camera.cameraonboarding.exception.LocationNotAddedException; import com.onboarding.camera.cameraonboarding.service.CameraService; import org.hamcrest.CoreMatchers; import org.junit.jupiter.api.Assertions; @@ -36,7 +40,7 @@ @WebMvcTest(controllers = CameraRestController.class) @AutoConfigureMockMvc(addFilters = false) @ExtendWith(MockitoExtension.class) -@Import(CameraDtoConverter.class) +@Import({CameraDtoConverter.class, LocationDtoConverter.class}) class CameraRestControllerTest { @Autowired @@ -52,12 +56,19 @@ class CameraRestControllerTest { private Camera camera; + private LocationDto locationDto; + + private Location location; + private final String CAMERA_NAME = "Camera 1"; private final String FIRMWARE_VERSION = "v1.0"; private final UUID CAMERA_ID = UUID.randomUUID(); private final String INVALID_UUID = "invalid-uuid"; private final UUID IMAGE_ID = UUID.randomUUID(); private final String IMAGE_DATA = "iVBORw0KGgoAAAANSUhEUgAAAAIAAAAECAYAAACk7+45AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAXSURBVBhXY5STV/rPAARMIAIE0BkMDAAtLgFmLE0FhAAAAABJRU5ErkJggg=="; + private final Double LATITUDE = 51.232; + private final Double LONGITUDE = -51.232; + private final String ADDRESS = "long enough address"; @BeforeEach void setUp() { @@ -65,6 +76,15 @@ void setUp() { cameraDto.setCameraName(CAMERA_NAME); cameraDto.setFirmwareVersion(FIRMWARE_VERSION); camera = new Camera(); + locationDto = new LocationDto(); + locationDto.setLatitude(LATITUDE); + locationDto.setLongitude(LONGITUDE); + locationDto.setAddress(ADDRESS); + location = new Location(); + location.setLatitude(locationDto.getLatitude()); + location.setLongitude(locationDto.getLongitude()); + location.setAddress(locationDto.getAddress()); + } @Test @@ -393,4 +413,127 @@ public void expect_handleDownloadImage_withNotInitializedCamera_returnInternalSe Mockito.verify(cameraService).handleDownloadImage(CAMERA_ID); } + + @Test + void expect_handleAddLocation_withValidInput_returnCreated() throws Exception { + // arrange + camera.setLocation(location); + Mockito.when(cameraService.handleAddLocation(CAMERA_ID, locationDto)) + .thenReturn(camera); + + // act + ResultActions response = mockMvc.perform(MockMvcRequestBuilders + .post("/api/v1/camera/{cameraId}/location", CAMERA_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(locationDto))); + + // assert + response.andExpect(MockMvcResultMatchers.status().isCreated()) + .andExpect(MockMvcResultMatchers.jsonPath("$.latitude", CoreMatchers.is(locationDto.getLatitude()))) + .andExpect(MockMvcResultMatchers.jsonPath("$.longitude", CoreMatchers.is(locationDto.getLongitude()))) + .andExpect(MockMvcResultMatchers.jsonPath("$.address", CoreMatchers.is(locationDto.getAddress()))); + + Mockito.verify(cameraService).handleAddLocation(CAMERA_ID, locationDto); + } + + @Test + void expect_handleAddLocation_withInvalidUUID_returnClientError() throws Exception { + // act + ResultActions response = mockMvc.perform(MockMvcRequestBuilders + .post("/api/v1/camera/{cameraId}/location", INVALID_UUID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(locationDto))); + + // assert + response.andExpect(MockMvcResultMatchers.status().is4xxClientError()); + + Mockito.verify(cameraService, Mockito.never()).handleAddLocation(Mockito.any(), Mockito.any()); + } + + @Test + void expect_handleAddLocation_withInvalidLatitude_returnBadRequest() throws Exception { + // arrange + locationDto.setLatitude(1800.0); + + // act + ResultActions response = mockMvc.perform(MockMvcRequestBuilders + .post("/api/v1/camera/{cameraId}/location", CAMERA_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(locationDto))); + + // assert + response.andExpect(MockMvcResultMatchers.status().isBadRequest()); + + Mockito.verify(cameraService, Mockito.never()).handleAddLocation(Mockito.any(), Mockito.any()); + } + + @Test + void expect_handleAddLocation_withInvalidLongitude_returnBadRequest() throws Exception { + // arrange + locationDto.setLongitude(1800.0); + + // act + ResultActions response = mockMvc.perform(MockMvcRequestBuilders + .post("/api/v1/camera/{cameraId}/location", CAMERA_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(locationDto))); + + // assert + response.andExpect(MockMvcResultMatchers.status().isBadRequest()); + + Mockito.verify(cameraService, Mockito.never()).handleAddLocation(Mockito.any(), Mockito.any()); + } + + @Test + void expect_handleAddLocation_withBlankAddress_returnBadRequest() throws Exception { + // arrange + locationDto.setAddress(""); + + // act + ResultActions response = mockMvc.perform(MockMvcRequestBuilders + .post("/api/v1/camera/{cameraId}/location", CAMERA_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(locationDto))); + + // assert + response.andExpect(MockMvcResultMatchers.status().isBadRequest()); + + Mockito.verify(cameraService, Mockito.never()).handleAddLocation(Mockito.any(), Mockito.any()); + } + + @Test + void expect_handleAddLocation_withCameraNotFound_returnNotFound() throws Exception { + // arrange + Mockito.doThrow(new CameraNotFoundException("Camera not found with id: " + CAMERA_ID)) + .when(cameraService).handleAddLocation(CAMERA_ID, locationDto); + + // act + ResultActions response = mockMvc.perform(MockMvcRequestBuilders + .post("/api/v1/camera/{cameraId}/location", CAMERA_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(locationDto))); + + // assert + response.andExpect(MockMvcResultMatchers.status().isNotFound()); + + Mockito.verify(cameraService).handleAddLocation(ArgumentMatchers.eq(CAMERA_ID), ArgumentMatchers.eq(locationDto)); + } + + @Test + void expect_handleAddLocation_withLocationNotAddedException_returnInternalServerError() throws Exception { + // arrange + Mockito.doThrow(new LocationNotAddedException("Error occurred while adding location")) + .when(cameraService).handleAddLocation(CAMERA_ID, locationDto); + + // act + ResultActions response = mockMvc.perform(MockMvcRequestBuilders + .post("/api/v1/camera/{cameraId}/location", CAMERA_ID) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(locationDto))); + + // assert + response.andExpect(MockMvcResultMatchers.status().isInternalServerError()); + + Mockito.verify(cameraService).handleAddLocation(ArgumentMatchers.eq(CAMERA_ID), ArgumentMatchers.eq(locationDto)); + } } \ No newline at end of file diff --git a/src/test/java/com/onboarding/camera/cameraonboarding/converter/LocationDtoConverterTest.java b/src/test/java/com/onboarding/camera/cameraonboarding/converter/LocationDtoConverterTest.java new file mode 100644 index 0000000..80e3c17 --- /dev/null +++ b/src/test/java/com/onboarding/camera/cameraonboarding/converter/LocationDtoConverterTest.java @@ -0,0 +1,39 @@ +package com.onboarding.camera.cameraonboarding.converter; + +import com.onboarding.camera.cameraonboarding.dto.LocationResponse; +import com.onboarding.camera.cameraonboarding.entity.Location; +import org.assertj.core.api.AssertionsForClassTypes; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class LocationDtoConverterTest { + + private LocationDtoConverter locationDtoConverter; + + private final Double LATITUDE = 51.232; + private final Double LONGITUDE = -51.232; + private final String ADDRESS = "long enough address"; + + @BeforeEach + void setUp() { + locationDtoConverter = new LocationDtoConverter(); + } + + @Test + void expect_convert_withValidLocation_returnLocationResponse() { + // arrange + Location location = new Location(); + location.setLatitude(LATITUDE); + location.setLongitude(LONGITUDE); + location.setAddress(ADDRESS); + + // act + LocationResponse response = locationDtoConverter.toLocationResponse(location); + + // assert + AssertionsForClassTypes.assertThat(response).isNotNull(); + AssertionsForClassTypes.assertThat(response.getLatitude()).isEqualTo(LATITUDE); + AssertionsForClassTypes.assertThat(response.getLongitude()).isEqualTo(LONGITUDE); + AssertionsForClassTypes.assertThat(response.getAddress()).isEqualTo(ADDRESS); + } +} \ No newline at end of file diff --git a/src/test/java/com/onboarding/camera/cameraonboarding/service/CameraServiceImplTest.java b/src/test/java/com/onboarding/camera/cameraonboarding/service/CameraServiceImplTest.java index e80a86a..753084c 100644 --- a/src/test/java/com/onboarding/camera/cameraonboarding/service/CameraServiceImplTest.java +++ b/src/test/java/com/onboarding/camera/cameraonboarding/service/CameraServiceImplTest.java @@ -1,6 +1,8 @@ package com.onboarding.camera.cameraonboarding.service; +import com.onboarding.camera.cameraonboarding.dto.LocationDto; import com.onboarding.camera.cameraonboarding.entity.Camera; +import com.onboarding.camera.cameraonboarding.entity.Location; import com.onboarding.camera.cameraonboarding.exception.CameraAlreadyInitializedException; import com.onboarding.camera.cameraonboarding.exception.CameraNotCreatedException; import com.onboarding.camera.cameraonboarding.exception.CameraNotFoundException; @@ -9,6 +11,7 @@ import com.onboarding.camera.cameraonboarding.exception.ImageNotDownloadedException; import com.onboarding.camera.cameraonboarding.exception.ImageNotFoundException; import com.onboarding.camera.cameraonboarding.exception.ImageNotUploadedException; +import com.onboarding.camera.cameraonboarding.exception.LocationNotAddedException; import com.onboarding.camera.cameraonboarding.repository.CameraRepository; import com.onboarding.camera.cameraonboarding.service.impl.BlobStorageServiceImpl; import com.onboarding.camera.cameraonboarding.service.impl.CameraServiceImpl; @@ -52,6 +55,8 @@ class CameraServiceImplTest { private ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + private LocationDto locationDto; + private final String CAMERA_NAME = "Camera 1"; private final String FIRMWARE_VERSION = "v1.0"; private final UUID CAMERA_ID = UUID.randomUUID(); @@ -64,6 +69,9 @@ class CameraServiceImplTest { private final String CONTAINER_NAME = "test_container"; private final UUID IMAGE_ID = UUID.randomUUID(); private final UUID NULL_UUID = null; + private final Double LATITUDE = 51.232; + private final Double LONGITUDE = -51.232; + private final String ADDRESS = "long enough address"; @BeforeEach void setUp() { @@ -73,6 +81,7 @@ void setUp() { camera.setFirmwareVersion(FIRMWARE_VERSION); camera.setCreatedAt(CREATED_AT); camera.setOnboardedAt(ONBOARDED_AT); + locationDto = new LocationDto(); } @Test @@ -88,13 +97,24 @@ void expect_handleSaveCamera_withValidCamera_returnSavedCamera() { final Camera savedCamera = cameraService.handleSaveCamera(camera); // assert - Assertions.assertThat(savedCamera).isNotNull(); - Assertions.assertThat(savedCamera.getCreatedAt()).isNotNull(); - Assertions.assertThat(savedCamera.getCreatedAt()).isEqualTo(CREATED_AT); - Assertions.assertThat(savedCamera.getCameraName()).isNotNull(); - Assertions.assertThat(savedCamera.getCameraName()).isEqualTo(CAMERA_NAME); - Assertions.assertThat(savedCamera.getFirmwareVersion()).isNotNull(); - Assertions.assertThat(savedCamera.getFirmwareVersion()).isEqualTo(FIRMWARE_VERSION); + Assertions.assertThat(savedCamera) + .isNotNull() + .satisfies(cam -> { + Assertions.assertThat(cam.getCreatedAt()) + .as("Check Created At") + .isNotNull() + .isEqualTo(CREATED_AT); + + Assertions.assertThat(cam.getCameraName()) + .as("Check Camera Name") + .isNotNull() + .isEqualTo(CAMERA_NAME); + + Assertions.assertThat(cam.getFirmwareVersion()) + .as("Check Fırmware Version") + .isNotNull() + .isEqualTo(FIRMWARE_VERSION); + }); Mockito.verify(dateTimeFactory, Mockito.times(2)).now(); Mockito.verify(cameraRepository).save(camera); @@ -405,4 +425,68 @@ void expect_handleDownloadImage_withNotInitializedCamera_throwsException() { Mockito.verify(blobStorageService, Mockito.never()).getBlob(Mockito.any(), Mockito.anyString(), Mockito.anyString()); } + + @Test + void expect_handleAddLocation_withValidLocation_returnCamera() { + // arrange + locationDto.setLatitude(LATITUDE); + locationDto.setLongitude(LONGITUDE); + locationDto.setAddress(ADDRESS); + Mockito.when(cameraRepository.findById(CAMERA_ID)).thenReturn(Optional.of(camera)); + + // act + Camera updatedCamera = cameraService.handleAddLocation(CAMERA_ID, locationDto); + Location location = updatedCamera.getLocation(); + + // assert + Assertions.assertThat(location) + .isNotNull() + .satisfies(loc -> { + Assertions.assertThat(loc.getLatitude()) + .as("Check Latitude") + .isNotNull() + .isEqualTo(LATITUDE); + + Assertions.assertThat(loc.getLongitude()) + .as("Check Longitude") + .isNotNull() + .isEqualTo(LONGITUDE); + + Assertions.assertThat(loc.getAddress()) + .as("Check Address") + .isNotNull() + .isEqualTo(ADDRESS); + }); + + Mockito.verify(cameraRepository).save(camera); + Mockito.verify(cameraRepository).findById(CAMERA_ID); + } + + @Test + void expect_handleAddLocation_withNonExistingCamera_throwsException() { + // arrange + Mockito.when(cameraRepository.findById(CAMERA_ID)).thenReturn(Optional.empty()); + + // act and assert + Assertions.assertThatThrownBy(() -> cameraService.handleAddLocation(CAMERA_ID, locationDto)) + .isInstanceOf(CameraNotFoundException.class) + .hasMessageContaining("Camera not found with id: " + CAMERA_ID); + + Mockito.verify(cameraRepository).findById(CAMERA_ID); + } + + @Test + void expect_handleAddLocation_withLocationNotAddedError_throwsException() { + // arrange + Mockito.when(cameraRepository.findById(CAMERA_ID)).thenReturn(Optional.of(camera)); + Mockito.doThrow(new LocationNotAddedException("Error occurred while adding location")) + .when(cameraRepository).save(camera); + + // act and assert + Assertions.assertThatThrownBy(() -> cameraService.handleAddLocation(CAMERA_ID, locationDto)) + .isInstanceOf(LocationNotAddedException.class) + .hasMessageContaining("Error occurred while adding location"); + + Mockito.verify(cameraRepository).findById(CAMERA_ID); + } }