Skip to content

Commit

Permalink
CDPS-1269 Add endpoint to update distinguishing mark image (#2364)
Browse files Browse the repository at this point in the history
Co-authored-by: Jon Brighton <brightonsbox@hotmail.com>
  • Loading branch information
scottrowley and brightonsbox authored Feb 28, 2025
1 parent a19d340 commit a4f7d9b
Show file tree
Hide file tree
Showing 4 changed files with 172 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,54 @@ class DistinguishingMarkResource(
.body(image)
}

@ApiResponses(
ApiResponse(responseCode = "200", description = "OK"),
ApiResponse(
responseCode = "400",
description = "Invalid request.",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))],
),
ApiResponse(
responseCode = "403",
description = "PRISON_API__PRISONER_PROFILE__RW role required to access endpoint",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))],
),
ApiResponse(
responseCode = "404",
description = "Requested resource not found.",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))],
),
ApiResponse(
responseCode = "500",
description = "Unrecoverable error occurred whilst processing request.",
content = [Content(mediaType = "application/json", schema = Schema(implementation = ErrorResponse::class))],
),
)
@Operation(
summary = "Update the content of an image",
description = "Requires role PRISON_API__PRISONER_PROFILE__RW",
)
@PreAuthorize("hasRole('PRISON_API__PRISONER_PROFILE__RW')")
@PutMapping(
value = ["/photo/{photoId}/image"],
consumes = [MediaType.MULTIPART_FORM_DATA_VALUE],
produces = [MediaType.APPLICATION_JSON_VALUE],
)
fun updateImage(
@PathVariable("photoId") @Parameter(
description = "The id of the image",
required = true,
) photoId: Long,
@Parameter(description = "The image as a file to upload", required = true) @RequestPart("file") file: MultipartFile,
): ResponseEntity<ByteArray> {
distinguishingMarkService.updatePhotoImage(photoId, file.inputStream)
val image = imageService.getImageContent(photoId, true)
.orElseThrow(EntityNotFoundException("Unable to find image with id $photoId"))
return ResponseEntity.ok()
.header("Content-Type", MediaType.IMAGE_JPEG_VALUE)
.body(image)
}

@ApiResponses(
ApiResponse(responseCode = "200", description = "OK"),
ApiResponse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,17 @@ class DistinguishingMarkService(
saveImage(mark, image)
}

@Transactional
fun updatePhotoImage(imageId: Long, image: InputStream) {
val imageContent = image.readAllBytes()
imageRepository.findById(imageId)
.orElseThrow { EntityNotFoundException.withMessage("Image with id $imageId not found") }
.let {
it.fullSizeImage = imageContent
it.thumbnailImage = scaleImage(imageContent)
}
}

private fun saveImage(mark: OffenderIdentifyingMark, image: InputStream) {
val imageContent = image.readAllBytes()
val newImage = OffenderImage
Expand Down Expand Up @@ -169,7 +180,7 @@ class DistinguishingMarkService(
}

@Throws(IOException::class, IllegalArgumentException::class, InterruptedException::class)
private fun scaleImage(source: ByteArray, width: Int = 150, height: Int = 200): ByteArray {
private fun scaleImage(source: ByteArray, width: Int = THUMBNAIL_WIDTH, height: Int = THUMBNAIL_HEIGHT): ByteArray {
val inputStream = ByteArrayInputStream(source)
val original = ImageIO.read(inputStream)
val scaled = original.getScaledInstance(width, height, Image.SCALE_DEFAULT)
Expand All @@ -196,5 +207,7 @@ class DistinguishingMarkService(

private companion object {
private val log: Logger = LoggerFactory.getLogger(this::class.java)
private const val THUMBNAIL_WIDTH = 150
private const val THUMBNAIL_HEIGHT = 200
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,90 @@ class DistinguishingMarkResourceIntTest : ResourceTest() {
}
}

@Nested
@DisplayName("PUT /api/person/photo/{photoId}/image")
inner class UpdateImageContent {

@Test
fun `returns 401 when user does not have a token`() {
webTestClient.put().uri("/api/person/photo/-103/image")
.body(multiPartFormRequest())
.exchange()
.expectStatus().isUnauthorized
}

@Test
fun `returns 403 if does not have override role`() {
webTestClient.put().uri("/api/person/photo/-103/image")
.headers(setClientAuthorisation(listOf()))
.body(multiPartFormRequest())
.exchange()
.expectStatus().isForbidden
}

@Test
fun `returns 403 when client has incorrect role`() {
webTestClient.put().uri("/api/person/photo/-103/image")
.headers(setClientAuthorisation(listOf("ROLE_SYSTEM_USER")))
.body(multiPartFormRequest())
.exchange()
.expectStatus().isForbidden
}

@Test
fun `returns 403 if not in user caseload`() {
webTestClient.put().uri("/api/person/photo/-103/image")
.headers(setAuthorisation("WAI_USER", listOf()))
.body(multiPartFormRequest())
.exchange()
.expectStatus().isForbidden
}

@Test
fun `returns 403 if user has no caseloads`() {
webTestClient.put().uri("/api/person/photo/-103/image")
.headers(setAuthorisation("RO_USER", listOf()))
.body(multiPartFormRequest())
.exchange()
.expectStatus().isForbidden
}

@Test
fun `returns 404 if image not found`() {
webTestClient.put().uri("/api/person/photo/-999/image")
.headers(setClientAuthorisation(listOf("PRISON_API__PRISONER_PROFILE__RW")))
.body(multiPartFormRequest())
.exchange()
.expectStatus().isNotFound
}

@Test
fun `Adds the photo to the mark`() {
val token = authTokenHelper.getToken(PRISONER_PROFILE_RW)
val parameters: MultiValueMap<String, Any> = LinkedMultiValueMap()
parameters.add("file", FileSystemResource(File(javaClass.getResource("/images/image.jpg")!!.file)))
val httpEntity = createHttpEntity(
token,
parameters,
contentType = MULTIPART_FORM_DATA_VALUE,
)

val response = testRestTemplate.exchange(
"/api/person/photo/-103/image",
PUT,
httpEntity,
object : ParameterizedTypeReference<String?>() {},
)

assertThatStatus(response, 200)
Assertions.assertThat(response.body).isNotEmpty()
}

private fun multiPartFormRequest(): BodyInserters.MultipartInserter = LinkedMultiValueMap<String, FileSystemResource>()
.apply { add("file", FileSystemResource(File(javaClass.getResource("/images/image.jpg")!!.file))) }
.let { BodyInserters.fromMultipartData(it) }
}

@Nested
@DisplayName("POST /api/person/{prisonerNumber}/distinguishing-mark/{seqId}/photo")
inner class AddPhotoToMark {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify
import org.mockito.kotlin.verifyNoMoreInteractions
import org.mockito.kotlin.whenever
import uk.gov.justice.hmpps.prison.api.model.DistinguishingMark
import uk.gov.justice.hmpps.prison.api.model.DistinguishingMarkDetails
Expand Down Expand Up @@ -240,6 +241,31 @@ class DistinguishingMarkServiceImplTest {
assertThat(savedImage.thumbnailImage).isNotEmpty()
}

@Nested
@DisplayName("Update existing image")
inner class UpdateExistingImage {
@Test
fun `Throws exception when image is not found`() {
whenever(imageRepository.findById(any())).thenReturn(Optional.empty())

val ex = assertThrows<RuntimeException> { service.updatePhotoImage(123L, ByteArrayInputStream(IMAGE_DATA)) }
assertThat(ex.message).isEqualTo("Image with id 123 not found")
}

@Test
fun `Updates image and thumbnail successfully`() {
val imageId = 123L
val image: OffenderImage = mock()
whenever(imageRepository.findById(imageId)).thenReturn(Optional.of(image))

service.updatePhotoImage(imageId, ByteArrayInputStream(IMAGE_DATA))

verify(image).fullSizeImage = IMAGE_DATA
verify(image).thumbnailImage = any()
verifyNoMoreInteractions(image)
}
}

@Nested
@DisplayName("Update existing distinguishing mark")
inner class UpdateExistingMark {
Expand Down

0 comments on commit a4f7d9b

Please sign in to comment.