Skip to content

Commit

Permalink
EROPSPT-390: Add endpoint for VAC photos
Browse files Browse the repository at this point in the history
  • Loading branch information
kirstenland committed Nov 14, 2024
1 parent 0fcf29a commit a20261c
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ class CertificateSummaryDtoMapper {
vacNumber = certificate.vacNumber!!,
applicationReference = certificate.applicationReference!!,
sourceReference = certificate.sourceReference!!,
photoLocationArn = certificate.photoLocationArn!!,
firstName = mostRecentPrintRequest.firstName!!,
middleNames = mostRecentPrintRequest.middleNames,
surname = mostRecentPrintRequest.surname!!,
printRequests = toPrintRequests(certificate.printRequests)
printRequests = toPrintRequests(certificate.printRequests),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ data class CertificateSummaryDto(
val vacNumber: String,
val sourceReference: String,
val applicationReference: String,
val photoLocationArn: String,
val firstName: String,
val middleNames: String?,
val surname: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@ import uk.gov.dluhc.printapi.mapper.CertificateSummaryResponseMapper
import uk.gov.dluhc.printapi.mapper.CertificateSummarySearchResponseMapper
import uk.gov.dluhc.printapi.models.CertificateSearchSummaryResponse
import uk.gov.dluhc.printapi.models.CertificateSummaryResponse
import uk.gov.dluhc.printapi.models.PreSignedUrlResourceResponse
import uk.gov.dluhc.printapi.service.CertificateSummarySearchService
import uk.gov.dluhc.printapi.service.CertificateSummaryService
import uk.gov.dluhc.printapi.service.S3AccessService

@RestController
@CrossOrigin
class CertificateSummaryController(
private val certificateSummaryService: CertificateSummaryService,
private val certificateSearchSummaryService: CertificateSummarySearchService,
private val s3AccessService: S3AccessService,
private val certificateSummaryResponseMapper: CertificateSummaryResponseMapper,
private val certificateSummarySearchResponseMapper: CertificateSummarySearchResponseMapper,
private val certificateSearchQueryStringParametersMapper: CertificateSearchQueryStringParametersMapper
Expand All @@ -35,6 +38,17 @@ class CertificateSummaryController(
.let { certificateSummaryResponseMapper.toCertificateSummaryResponse(it) }
}

@GetMapping("/eros/{eroId}/certificates/photo")
@PreAuthorize(HAS_ERO_VC_ADMIN_AUTHORITY)
fun getCertificatePhoto(
@PathVariable eroId: String,
@RequestParam applicationId: String,
): PreSignedUrlResourceResponse {
val certificate = certificateSummaryService.getCertificateSummary(eroId, VOTER_CARD, applicationId)
val preSignedUrl = s3AccessService.generatePresignedGetCertificatePhotoUrl(certificate.photoLocationArn)
return PreSignedUrlResourceResponse(preSignedUrl = preSignedUrl)
}

@GetMapping("/eros/{eroId}/certificates/search")
@PreAuthorize(HAS_ERO_VC_ADMIN_AUTHORITY)
fun searchCertificates(
Expand Down
81 changes: 80 additions & 1 deletion src/main/resources/openapi/PrintAPIs.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.0.0
info:
title: Print APIs
version: '1.23.1'
version: '1.23.2'
description: Print APIs
#
# --------------------------------------------------------------------------------
Expand Down Expand Up @@ -95,6 +95,85 @@ paths:
connectionId: '${vpc_connection_id}'
httpMethod: GET

'/eros/{eroId}/certificates/photo':
parameters:
- name: eroId
description: The ID of the Electoral Registration Office responsible for the application.
schema:
type: string
in: path
required: true
options:
summary: CORS support
description: |
Enable CORS by returning correct headers
tags:
- Voter Authority Certificates (VAC)
responses:
200:
description: Default response for CORS method
headers:
Access-Control-Allow-Origin:
schema:
type: string
Access-Control-Allow-Methods:
schema:
type: string
Access-Control-Allow-Headers:
schema:
type: string
content: { }
x-amazon-apigateway-integration:
type: mock
requestTemplates:
application/json: |
{
"statusCode" : 200
}
responses:
default:
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Headers: '''Content-Type,X-Amz-Date,Authorization,X-Api-Key'''
method.response.header.Access-Control-Allow-Methods: '''*'''
method.response.header.Access-Control-Allow-Origin: '''*'''
responseTemplates:
application/json: |
{}
get:
summary: Returns the Voter Authority Certificate photo for a VAC application
description: Returns the Voter Authority Certificate photo for a VAC application
tags:
- Voter Authority Certificates (VAC)
parameters:
- name: applicationId
description: An identifier of a Voter Authority Certificate application
schema:
type: string
in: query
required: true
responses:
'200':
$ref: '#/components/responses/PreSignedUrlResource'
'404':
$ref: '#/components/responses/404ErrorResponse'
operationId: get-voter-authority-certificate-photo-by-application-id
security:
- eroUserCognitoUserPoolAuthorizer: [ ]
x-amazon-apigateway-integration:
type: HTTP_PROXY
uri: '${base_uri}/eros/{eroId}/certificates/photo'
requestParameters:
integration.request.path.eroId: method.request.path.eroId
integration.request.header.x-request-id: context.requestId
responseParameters:
method.response.header.Access-Control-Allow-Headers: '''Content-Type,X-Amz-Date,Authorization,X-Api-Key'''
method.response.header.Access-Control-Allow-Methods: '''*'''
method.response.header.Access-Control-Allow-Origin: '''*'''
connectionType: VPC_LINK
connectionId: '${vpc_connection_id}'
httpMethod: GET

'/eros/{eroId}/certificates/search':
parameters:
- name: eroId
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ internal class CertificateSummaryDtoMapperTest {
firstName = printRequest.firstName!!,
middleNames = printRequest.middleNames,
surname = printRequest.surname!!,
photoLocationArn = certificate.photoLocationArn!!,
printRequests = listOf(
buildPrintRequestSummaryDto(
userId = printRequest.userId!!,
Expand Down Expand Up @@ -155,6 +156,7 @@ internal class CertificateSummaryDtoMapperTest {
firstName = printRequest2.firstName!!,
middleNames = printRequest2.middleNames,
surname = printRequest2.surname!!,
photoLocationArn = certificate.photoLocationArn!!,
printRequests = listOf(
PrintRequestSummaryDto(
status = PrintRequestStatusDto.valueOf(expectedStatus2.name),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package uk.gov.dluhc.printapi.rest

import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test
import org.springframework.http.MediaType.APPLICATION_JSON
import uk.gov.dluhc.printapi.config.IntegrationTest
import uk.gov.dluhc.printapi.config.LocalStackContainerConfiguration
import uk.gov.dluhc.printapi.database.entity.SourceType
import uk.gov.dluhc.printapi.models.ErrorResponse
import uk.gov.dluhc.printapi.models.PreSignedUrlResourceResponse
import uk.gov.dluhc.printapi.testsupport.addCertificatePhotoToS3
import uk.gov.dluhc.printapi.testsupport.assertj.assertions.models.ErrorResponseAssert
import uk.gov.dluhc.printapi.testsupport.bearerToken
import uk.gov.dluhc.printapi.testsupport.buildS3Arn
import uk.gov.dluhc.printapi.testsupport.matchingPreSignedAwsS3GetUrl
import uk.gov.dluhc.printapi.testsupport.testdata.anotherValidEroId
import uk.gov.dluhc.printapi.testsupport.testdata.entity.buildCertificate
import uk.gov.dluhc.printapi.testsupport.testdata.getVCAdminBearerToken
import uk.gov.dluhc.printapi.testsupport.testdata.model.buildElectoralRegistrationOfficeResponse
import uk.gov.dluhc.printapi.testsupport.testdata.model.buildLocalAuthorityResponse
import uk.gov.dluhc.printapi.testsupport.testdata.zip.aPhotoBucketPath

internal class GetVacPhotoByApplicationIdIntegrationTest : IntegrationTest() {
companion object {
private const val URI_TEMPLATE =
"/eros/{ERO_ID}/certificates/photo?applicationId={APPLICATION_ID}"
private const val APPLICATION_ID = "6407b6158f529a11713a1e5c"
private const val GSS_CODE = "W06000099"
}

@Test
fun `should return bad request given request without applicationId query string parameter`() {
wireMockService.stubCognitoJwtIssuerResponse()

webTestClient.get()
.uri("/eros/{ERO_ID}/certificates/photo", ERO_ID)
.bearerToken(getVCAdminBearerToken(eroId = ERO_ID))
.contentType(APPLICATION_JSON)
.exchange()
.expectStatus()
.isBadRequest
}

@Test
fun `should return forbidden given user with valid bearer token belonging to a different ero`() {
wireMockService.stubCognitoJwtIssuerResponse()
val userGroupEroId = anotherValidEroId(ERO_ID)

webTestClient.get()
.uri(URI_TEMPLATE, ERO_ID, APPLICATION_ID)
.bearerToken(getVCAdminBearerToken(eroId = userGroupEroId))
.contentType(APPLICATION_JSON)
.exchange()
.expectStatus()
.isForbidden
}

@Test
fun `should return not found given no VAC exists for application ID`() {
// Given
val eroResponse = buildElectoralRegistrationOfficeResponse(
id = ERO_ID,
localAuthorities = listOf(buildLocalAuthorityResponse(gssCode = GSS_CODE), buildLocalAuthorityResponse())
)
wireMockService.stubCognitoJwtIssuerResponse()
wireMockService.stubEroManagementGetEroByEroId(eroResponse, ERO_ID)

// When
val response = webTestClient.get()
.uri(URI_TEMPLATE, ERO_ID, APPLICATION_ID)
.bearerToken(getVCAdminBearerToken(eroId = ERO_ID))
.contentType(APPLICATION_JSON)
.exchange()
.expectStatus()
.isNotFound
.returnResult(ErrorResponse::class.java)

// Then
val actual = response.responseBody.blockFirst()
ErrorResponseAssert.assertThat(actual)
.hasStatus(404)
.hasError("Not Found")
.hasMessage("Certificate for eroId = $ERO_ID with sourceType = VOTER_CARD and sourceReference = $APPLICATION_ID not found")
}

@Test
fun `should return a presigned url for the VAC photo`() {
// Given
val eroResponse = buildElectoralRegistrationOfficeResponse(
id = ERO_ID,
localAuthorities = listOf(buildLocalAuthorityResponse(gssCode = GSS_CODE), buildLocalAuthorityResponse())
)
wireMockService.stubCognitoJwtIssuerResponse()
wireMockService.stubEroManagementGetEroByEroId(eroResponse, ERO_ID)

val s3Bucket = LocalStackContainerConfiguration.VCA_TARGET_BUCKET
val s3Path = aPhotoBucketPath()
val certificate = buildCertificate(
gssCode = eroResponse.localAuthorities[0].gssCode,
sourceType = SourceType.VOTER_CARD,
sourceReference = APPLICATION_ID,
photoLocationArn = buildS3Arn(s3Bucket, s3Path)
)
certificateRepository.save(certificate)
s3Client.addCertificatePhotoToS3(s3Bucket, s3Path)

// When
val response = webTestClient.get()
.uri(URI_TEMPLATE, ERO_ID, APPLICATION_ID)
.bearerToken(getVCAdminBearerToken(eroId = ERO_ID))
.contentType(APPLICATION_JSON)
.exchange()
.expectStatus()
.isOk
.returnResult(PreSignedUrlResourceResponse::class.java)

// Then
val actual = response.responseBody.blockFirst()
val expectedUrl = matchingPreSignedAwsS3GetUrl(s3Path)
assertThat(actual!!.preSignedUrl).matches {
it.toString()
.matches(expectedUrl)
}
}

@Test
fun `should return a presigned url for the VAC photo given object key contains special characters`() {
// Given
val eroResponse = buildElectoralRegistrationOfficeResponse(
id = ERO_ID,
localAuthorities = listOf(buildLocalAuthorityResponse(gssCode = GSS_CODE), buildLocalAuthorityResponse())
)
wireMockService.stubCognitoJwtIssuerResponse()
wireMockService.stubEroManagementGetEroByEroId(eroResponse, ERO_ID)
val s3Bucket = LocalStackContainerConfiguration.VCA_TARGET_BUCKET
val photoObjectKey = "dir1/Jane+!@£$%^&*())))_+-=[]{}'\\|;;<>,.:?#`~§± Doe:Awesome Company Ltd:HEADSHOT.jpg"
val certificate = buildCertificate(
gssCode = eroResponse.localAuthorities[0].gssCode,
sourceType = SourceType.VOTER_CARD,
sourceReference = APPLICATION_ID,
photoLocationArn = buildS3Arn(s3Bucket, photoObjectKey)
)
certificateRepository.save(certificate)
s3Client.addCertificatePhotoToS3(s3Bucket, photoObjectKey)
val expectedEncodedPath =
"dir1/Jane%2B%21%40%C2%A3%24%25%5E%26%2A%28%29%29%29%29_%2B-%3D%5B%5D%7B%7D%27%5C%7C%3B%3B%3C%3E%2C.%3A%3F%23%60~%C2%A7%C2%B1%20Doe%3AAwesome%20Company%20Ltd%3AHEADSHOT.jpg"

// When
val response = webTestClient.get()
.uri(URI_TEMPLATE, ERO_ID, APPLICATION_ID)
.bearerToken(getVCAdminBearerToken(eroId = ERO_ID))
.contentType(APPLICATION_JSON)
.exchange()
.expectStatus()
.isOk
.returnResult(PreSignedUrlResourceResponse::class.java)

// Then
val actual = response.responseBody.blockFirst()
assertThat(actual!!.preSignedUrl.rawPath).isEqualTo("/$s3Bucket/$expectedEncodedPath")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import uk.gov.dluhc.printapi.testsupport.testdata.aValidSourceReference
import uk.gov.dluhc.printapi.testsupport.testdata.aValidSurname
import uk.gov.dluhc.printapi.testsupport.testdata.aValidUserId
import uk.gov.dluhc.printapi.testsupport.testdata.aValidVacNumber
import uk.gov.dluhc.printapi.testsupport.testdata.zip.aPhotoArn
import java.time.Instant

fun buildCertificateSummaryDto(
Expand All @@ -21,6 +22,7 @@ fun buildCertificateSummaryDto(
firstName: String = aValidFirstName(),
surname: String = aValidSurname(),
middleNames: String? = null,
photoLocationArn: String = aPhotoArn(),
printRequests: List<PrintRequestSummaryDto> = mutableListOf(buildPrintRequestSummaryDto())
) = CertificateSummaryDto(
vacNumber = vacNumber,
Expand All @@ -29,7 +31,8 @@ fun buildCertificateSummaryDto(
firstName = firstName,
surname = surname,
middleNames = middleNames,
printRequests = printRequests
printRequests = printRequests,
photoLocationArn = photoLocationArn,
)

fun buildPrintRequestSummaryDto(
Expand Down

0 comments on commit a20261c

Please sign in to comment.