diff --git a/app/configuration/ApplicationConfig.scala b/app/configuration/ApplicationConfig.scala index 73f183d90..c170e895a 100644 --- a/app/configuration/ApplicationConfig.scala +++ b/app/configuration/ApplicationConfig.scala @@ -31,6 +31,8 @@ class ApplicationConfig @Inject() (configuration: Configuration) { val draftMetadataFileName: String = configuration.get[String]("draftMetadata.fileName") + val draftMetadataErrorFileName: String = configuration.get[String]("draftMetadata.errorFileName") + val notificationSnsTopicArn: String = get("notificationSnsTopicArn") val fileChecksTotalTimoutInSeconds: Int = configuration.get[Int]("fileChecksTotalTimoutInSeconds") diff --git a/app/controllers/DraftMetadataChecksResultsController.scala b/app/controllers/DraftMetadataChecksResultsController.scala index af4d2adc3..75a66ae75 100644 --- a/app/controllers/DraftMetadataChecksResultsController.scala +++ b/app/controllers/DraftMetadataChecksResultsController.scala @@ -4,10 +4,10 @@ import auth.TokenSecurity import configuration.{ApplicationConfig, KeycloakConfiguration} import graphql.codegen.GetConsignmentStatus.getConsignmentStatus.GetConsignment import org.pac4j.play.scala.SecurityComponents -import play.api.i18n.I18nSupport +import play.api.i18n.{I18nSupport, Messages} import play.api.mvc.{Action, AnyContent, Request} import services.Statuses._ -import services._ +import services.{FileError, _} import viewsapi.Caching.preventCaching import java.util.UUID @@ -20,7 +20,8 @@ class DraftMetadataChecksResultsController @Inject() ( val keycloakConfiguration: KeycloakConfiguration, val consignmentService: ConsignmentService, val applicationConfig: ApplicationConfig, - val consignmentStatusService: ConsignmentStatusService + val consignmentStatusService: ConsignmentStatusService, + val draftMetadataService: DraftMetadataService )(implicit val ec: ExecutionContext) extends TokenSecurity with I18nSupport { @@ -33,24 +34,74 @@ class DraftMetadataChecksResultsController @Inject() ( for { reference <- consignmentService.getConsignmentRef(consignmentId, request.token.bearerAccessToken) consignmentStatuses <- consignmentStatusService.getConsignmentStatuses(consignmentId, token) + errorType <- getErrorType(consignmentStatuses, consignmentId) } yield { - Ok( - views.html.draftmetadata - .draftMetadataChecksResults(consignmentId, reference, getValue(consignmentStatuses, DraftMetadataType), request.token.name) - ) - .uncache() + val resultsPage = { + // leaving original page for no errors + if (errorType == FileError.NONE) { + views.html.draftmetadata + .draftMetadataChecksResults(consignmentId, reference, DraftMetadataProgress("IMPORTED", "blue"), request.token.name) + } else { + if (isErrorReportAvailable(errorType)) { + views.html.draftmetadata + .draftMetadataChecksWithErrorDownload( + consignmentId, + reference, + request.token.name, + actionMessage(errorType), + detailsMessage(errorType) + ) + } else { + views.html.draftmetadata + .draftMetadataChecksErrorsNoDownload( + consignmentId, + reference, + request.token.name, + actionMessage(errorType), + detailsMessage(errorType) + ) + } + } + } + Ok(resultsPage).uncache() } } } - def getValue(statuses: List[GetConsignment.ConsignmentStatuses], statusType: StatusType): DraftMetadataProgress = { - val failed = DraftMetadataProgress("FAILED", "red") - statuses.find(_.statusType == statusType.id).map(_.value).map { - case FailedValue.value => failed - case CompletedWithIssuesValue.value => DraftMetadataProgress("ERRORS", "red") - case CompletedValue.value => DraftMetadataProgress("IMPORTED", "blue") - case _ => failed - } getOrElse failed + private def actionMessage(fileError: FileError.FileError)(implicit messages: Messages): String = { + val key = s"draftMetadata.validation.action.$fileError" + if (Messages.isDefinedAt(key)) + Messages(key) + else + s"Require action message for $key" + } + + private def detailsMessage(fileError: FileError.FileError)(implicit messages: Messages): String = { + val key = s"draftMetadata.validation.details.$fileError" + if (Messages.isDefinedAt(key)) + Messages(key) + else + s"Require details message for $key" + } + + private def isErrorReportAvailable(fileError: FileError.FileError): Boolean = { + fileError match { + case FileError.SCHEMA_VALIDATION => true + case _ => false + } + } + + private def getErrorType(consignmentStatuses: List[GetConsignment.ConsignmentStatuses], consignmentId: UUID): Future[FileError.Value] = { + val draftMetadataStatus = consignmentStatuses.find(_.statusType == DraftMetadataType.id).map(_.value) + if (draftMetadataStatus.isDefined) { + draftMetadataStatus.get match { + case CompletedValue.value => Future.successful(FileError.NONE) + case FailedValue.value => Future.successful(FileError.UNKNOWN) + case _ => draftMetadataService.getErrorTypeFromErrorJson(consignmentId) + } + } else { + Future.successful(FileError.UNKNOWN) + } } } diff --git a/app/services/DownloadService.scala b/app/services/DownloadService.scala new file mode 100644 index 000000000..3d19d37bf --- /dev/null +++ b/app/services/DownloadService.scala @@ -0,0 +1,25 @@ +package services + +import configuration.ApplicationConfig +import software.amazon.awssdk.core.ResponseBytes +import software.amazon.awssdk.core.async.AsyncResponseTransformer +import software.amazon.awssdk.services.s3.S3AsyncClient +import software.amazon.awssdk.services.s3.model.{GetObjectRequest, GetObjectResponse} +import uk.gov.nationalarchives.aws.utils.s3.S3Clients._ + +import javax.inject.Inject +import scala.concurrent.{ExecutionContext, Future} +import scala.jdk.FutureConverters.CompletionStageOps + +class DownloadService @Inject() (val applicationConfig: ApplicationConfig)(implicit val ec: ExecutionContext) { + private val s3Endpoint = applicationConfig.s3Endpoint + + def downloadFile(bucket: String, key: String): Future[ResponseBytes[GetObjectResponse]] = { + downloadFile(bucket, key, s3Async(s3Endpoint)) + } + + def downloadFile(bucket: String, key: String, s3AsyncClient: S3AsyncClient): Future[ResponseBytes[GetObjectResponse]] = { + val getObjectRequest = GetObjectRequest.builder.bucket(bucket).key(key).build() + s3AsyncClient.getObject(getObjectRequest, AsyncResponseTransformer.toBytes[GetObjectResponse]).asScala + } +} diff --git a/app/services/DraftMetadataService.scala b/app/services/DraftMetadataService.scala index 4f3c9fd8d..74d970ff6 100644 --- a/app/services/DraftMetadataService.scala +++ b/app/services/DraftMetadataService.scala @@ -1,13 +1,32 @@ package services import com.google.inject.Inject +import configuration.ApplicationConfig +import io.circe.Decoder +import io.circe.generic.auto._ +import io.circe.parser.decode import play.api.libs.ws.WSClient import play.api.{Configuration, Logging} +import software.amazon.awssdk.core.ResponseBytes +import software.amazon.awssdk.services.s3.model.GetObjectResponse +import uk.gov.nationalarchives.tdr.validation.Metadata +import java.nio.charset.StandardCharsets import java.util.UUID import scala.concurrent.{ExecutionContext, Future} -class DraftMetadataService @Inject() (val wsClient: WSClient, val configuration: Configuration)(implicit val executionContext: ExecutionContext) extends Logging { +object FileError extends Enumeration { + type FileError = Value + val UTF_8, INVALID_CSV, SCHEMA_REQUIRED, SCHEMA_VALIDATION, UNKNOWN, NONE = Value +} + +case class Error(validationProcess: String, property: String, errorKey: String, message: String) +case class ValidationErrors(assetId: String, errors: Set[Error], data: List[Metadata] = List.empty[Metadata]) +case class ErrorFileData(consignmentId: UUID, date: String, fileError: FileError.FileError, validationErrors: List[ValidationErrors]) + +class DraftMetadataService @Inject() (val wsClient: WSClient, val configuration: Configuration, val applicationConfig: ApplicationConfig, val downloadService: DownloadService)( + implicit val executionContext: ExecutionContext +) extends Logging { def triggerDraftMetadataValidator(consignmentId: UUID, uploadFileName: String, token: String): Future[Boolean] = { val url = s"${configuration.get[String]("metadatavalidation.baseUrl")}/draft-metadata/validate/$consignmentId/$uploadFileName" @@ -24,4 +43,17 @@ class DraftMetadataService @Inject() (val wsClient: WSClient, val configuration: } ) } + + def getErrorTypeFromErrorJson(consignmentId: UUID): Future[FileError.FileError] = { + implicit val FileErrorDecoder: Decoder[FileError.Value] = Decoder.decodeEnumeration(FileError) + val errorFile: Future[ResponseBytes[GetObjectResponse]] = + downloadService.downloadFile(applicationConfig.draft_metadata_s3_bucket_name, s"$consignmentId/${applicationConfig.draftMetadataErrorFileName}") + + errorFile + .map(responseBytes => { + val errorJson = new String(responseBytes.asByteArray(), StandardCharsets.UTF_8) + decode[ErrorFileData](errorJson).fold(_ => FileError.UNKNOWN, errorFileData => errorFileData.fileError) + }) + .recoverWith(_ => Future.successful(FileError.UNKNOWN)) + } } diff --git a/app/views/draftmetadata/draftMetadataChecksErrorsNoDownload.scala.html b/app/views/draftmetadata/draftMetadataChecksErrorsNoDownload.scala.html new file mode 100644 index 000000000..c3fdd85b6 --- /dev/null +++ b/app/views/draftmetadata/draftMetadataChecksErrorsNoDownload.scala.html @@ -0,0 +1,21 @@ +@import views.html.partials._ + +@import java.util.UUID +@(consignmentId: UUID, consignmentRef: String, name: String, actionMessage: String, detailsMessage: String)(implicit request: RequestHeader, messages: Messages) + +@main("Results of CSV Checks", name = name) { +
+
+ @draftMetadataChecksActionProcess(actionMessage, detailsMessage) +

Once you have addressed this issue upload a revised metadata file.

+ + + +
+ @transferReference(consignmentRef, isJudgmentUser = false) +
+} diff --git a/app/views/draftmetadata/draftMetadataChecksWithErrorDownload.scala.html b/app/views/draftmetadata/draftMetadataChecksWithErrorDownload.scala.html new file mode 100644 index 000000000..a6fc5f77c --- /dev/null +++ b/app/views/draftmetadata/draftMetadataChecksWithErrorDownload.scala.html @@ -0,0 +1,33 @@ +@import views.html.partials._ + +@import java.util.UUID +@(consignmentId: UUID, consignmentRef: String, name: String, actionMessage: String, detailsMessage: String)(implicit request: RequestHeader, messages: Messages) + + @main("Results of CSV Checks", name = name) { +
+
+ @draftMetadataChecksActionProcess(actionMessage, detailsMessage) + +

The report below contains details about issues found.

+ + + +

Once you have addressed this issue upload a revised metadata file.

+ + + +
+ @transferReference(consignmentRef, isJudgmentUser = false) +
+ } diff --git a/app/views/partials/draftMetadataChecksActionProcess.scala.html b/app/views/partials/draftMetadataChecksActionProcess.scala.html new file mode 100644 index 000000000..9d0684d56 --- /dev/null +++ b/app/views/partials/draftMetadataChecksActionProcess.scala.html @@ -0,0 +1,37 @@ +@(actionMessage: String, detailsMessage: String)(implicit request: RequestHeader, messages: Messages) + +

+ Results of your metadata checks +

+

There was an issue with your uploaded metadata file.

+ +

+ +
+
+
+ Status +
+
+ Issues found +
+
+ +
+
+ Details +
+
+ @detailsMessage +
+
+ +
+
+ Action +
+
+ @actionMessage +
+
+
diff --git a/conf/application.base.conf b/conf/application.base.conf index c29b90a8c..9a6e350a3 100644 --- a/conf/application.base.conf +++ b/conf/application.base.conf @@ -64,6 +64,7 @@ featureAccessBlock { draftMetadata { fileName = "draft-metadata.csv" + errorFileName = "draft-metadata-errors.json" } notificationSnsTopicArn = ${NOTIFICATION_SNS_TOPIC_ARN} diff --git a/conf/messages b/conf/messages index 385a76762..9ed9892ce 100644 --- a/conf/messages +++ b/conf/messages @@ -36,3 +36,5 @@ notification.savedProgress.heading=Your progress has been saved notification.savedProgress.metadataInfo=Your records and any metadata you added has been saved. You can leave at any time and return to this transfer by visiting the {1} page. additionalMetadata.descriptive.sensitive=If the description of a record contains sensitive information, you must enter the full uncensored version on the Descriptive metadata page before entering an alternative description on the Closure metadata page. +draftMetadata.validation.details.SCHEMA_VALIDATION=We found validation errors in the uploaded metadata. +draftMetadata.validation.action.SCHEMA_VALIDATION=Download the report below for details on individual validation errors. diff --git a/test/controllers/DraftMetadataChecksResultsControllerSpec.scala b/test/controllers/DraftMetadataChecksResultsControllerSpec.scala index 176225eee..6e50678df 100644 --- a/test/controllers/DraftMetadataChecksResultsControllerSpec.scala +++ b/test/controllers/DraftMetadataChecksResultsControllerSpec.scala @@ -3,6 +3,7 @@ package controllers import com.github.tomakehurst.wiremock.WireMockServer import configuration.{ApplicationConfig, GraphQLConfiguration, KeycloakConfiguration} import graphql.codegen.GetConsignmentStatus.getConsignmentStatus.{GetConsignment => gcs} +import org.mockito.ArgumentMatchers.any import org.mockito.Mockito.when import org.pac4j.play.scala.SecurityComponents import org.scalatest.matchers.should.Matchers._ @@ -12,12 +13,12 @@ import play.api.test.CSRFTokenHelper._ import play.api.test.FakeRequest import play.api.test.Helpers.{status => playStatus, _} import services.Statuses.{CompletedValue, CompletedWithIssuesValue, DraftMetadataType, FailedValue} -import services.{ConsignmentService, ConsignmentStatusService} +import services.{ConsignmentService, ConsignmentStatusService, DraftMetadataService, FileError} import testUtils.FrontEndTestHelper import java.time.{LocalDateTime, ZoneId, ZonedDateTime} import java.util.UUID -import scala.concurrent.ExecutionContext +import scala.concurrent.{ExecutionContext, Future} class DraftMetadataChecksResultsControllerSpec extends FrontEndTestHelper { implicit val ec: ExecutionContext = ExecutionContext.global @@ -59,57 +60,192 @@ class DraftMetadataChecksResultsControllerSpec extends FrontEndTestHelper { } "DraftMetadataChecksResultsController should render the page with the correct status" should { - val draftMetadataStatuses = Table( - ("status", "progress"), - (CompletedValue.value, DraftMetadataProgress("IMPORTED", "blue")), - (CompletedWithIssuesValue.value, DraftMetadataProgress("ERRORS", "red")), - (FailedValue.value, DraftMetadataProgress("FAILED", "red")) - ) - forAll(draftMetadataStatuses) { (statusValue, progress) => - s"render the draftMetadataResults page when the status is $statusValue" in { - val controller = instantiateController(blockDraftMetadataUpload = false) - val additionalMetadataEntryMethodPage = controller - .draftMetadataChecksResultsPage(consignmentId) - .apply(FakeRequest(GET, "/draft-metadata/checks-results").withCSRFToken) - setConsignmentTypeResponse(wiremockServer, "standard") - setConsignmentReferenceResponse(wiremockServer) - val someDateTime = ZonedDateTime.of(LocalDateTime.of(2022, 3, 10, 1, 0), ZoneId.systemDefault()) - val consignmentStatuses = List( - gcs.ConsignmentStatuses(UUID.randomUUID(), UUID.randomUUID(), DraftMetadataType.id, statusValue, someDateTime, None) - ) - setConsignmentStatusResponse(app.configuration, wiremockServer, consignmentStatuses = consignmentStatuses) - - val pageAsString = contentAsString(additionalMetadataEntryMethodPage) - - playStatus(additionalMetadataEntryMethodPage) mustBe OK - contentType(additionalMetadataEntryMethodPage) mustBe Some("text/html") - pageAsString must include("Results of CSV Checks - Transfer Digital Records - GOV.UK") - pageAsString must include( - """

- | Results of your metadata checks - |

""".stripMargin - ) - pageAsString must include( - s"""
+ s"render the draftMetadataResults page when the status is completed" in { + val controller = instantiateController(blockDraftMetadataUpload = false) + val additionalMetadataEntryMethodPage = controller + .draftMetadataChecksResultsPage(consignmentId) + .apply(FakeRequest(GET, "/draft-metadata/checks-results").withCSRFToken) + setConsignmentTypeResponse(wiremockServer, "standard") + setConsignmentReferenceResponse(wiremockServer) + val someDateTime = ZonedDateTime.of(LocalDateTime.of(2022, 3, 10, 1, 0), ZoneId.systemDefault()) + val consignmentStatuses = List( + gcs.ConsignmentStatuses(UUID.randomUUID(), UUID.randomUUID(), DraftMetadataType.id, CompletedValue.value, someDateTime, None) + ) + setConsignmentStatusResponse(app.configuration, wiremockServer, consignmentStatuses = consignmentStatuses) + + val pageAsString = contentAsString(additionalMetadataEntryMethodPage) + + playStatus(additionalMetadataEntryMethodPage) mustBe OK + contentType(additionalMetadataEntryMethodPage) mustBe Some("text/html") + pageAsString must include("Results of CSV Checks - Transfer Digital Records - GOV.UK") + pageAsString must include( + s"""

+ | Results of your metadata checks + |

""".stripMargin + ) + pageAsString must include( + s"""
|
|
| Status |
|
- | - | ${progress.value} + | + | ${DraftMetadataProgress("IMPORTED", "blue").value} | |
|
|
""".stripMargin - ) - pageAsString must include( - s"""
+ ) + pageAsString must include( + s""" """.stripMargin - ) + ) + + } + } + + "DraftMetadataChecksResultsController should render the error page with error report download button" should { + val draftMetadataStatuses = Table( + ("status", "fileError"), + (CompletedWithIssuesValue.value, FileError.SCHEMA_VALIDATION) + ) + forAll(draftMetadataStatuses) { (statusValue, fileError) => + { + s"render the draftMetadataResults page when the status is $statusValue" in { + val controller = instantiateController(blockDraftMetadataUpload = false, fileError = fileError) + val additionalMetadataEntryMethodPage = controller + .draftMetadataChecksResultsPage(consignmentId) + .apply(FakeRequest(GET, "/draft-metadata/checks-results").withCSRFToken) + setConsignmentTypeResponse(wiremockServer, "standard") + setConsignmentReferenceResponse(wiremockServer) + val someDateTime = ZonedDateTime.of(LocalDateTime.of(2022, 3, 10, 1, 0), ZoneId.systemDefault()) + val consignmentStatuses = List( + gcs.ConsignmentStatuses(UUID.randomUUID(), UUID.randomUUID(), DraftMetadataType.id, statusValue, someDateTime, None) + ) + setConsignmentStatusResponse(app.configuration, wiremockServer, consignmentStatuses = consignmentStatuses) + + val pageAsString = contentAsString(additionalMetadataEntryMethodPage) + + playStatus(additionalMetadataEntryMethodPage) mustBe OK + contentType(additionalMetadataEntryMethodPage) mustBe Some("text/html") + pageAsString must include("Results of CSV Checks - Transfer Digital Records - GOV.UK") + pageAsString must include( + """

+ | Results of your metadata checks + |

""".stripMargin + ) + pageAsString must include( + s"""
+ |
+ |
+ | Status + |
+ |
+ | Issues found + |
+ |
+ | + |
+ |
+ | Details + |
+ |
+ | Require details message for draftMetadata.validation.details.$fileError + |
+ |
+ | + |
+ |
+ | Action + |
+ |
+ | Require action message for draftMetadata.validation.action.$fileError + |
+ |
+ |
""".stripMargin + ) + pageAsString must include( + s"""

The report below contains details about issues found.

+ | + | """.stripMargin + ) + } + } + } + } + + "DraftMetadataChecksResultsController should render the error page with no error download for some errors" should { + val draftMetadataStatuses = Table( + ("status", "fileError"), + (FailedValue.value, FileError.UNKNOWN) + ) + forAll(draftMetadataStatuses) { (statusValue, fileError) => + { + s"render the draftMetadataResults page when the status is $statusValue" in { + val controller = instantiateController(blockDraftMetadataUpload = false, fileError = fileError) + val additionalMetadataEntryMethodPage = controller + .draftMetadataChecksResultsPage(consignmentId) + .apply(FakeRequest(GET, "/draft-metadata/checks-results").withCSRFToken) + setConsignmentTypeResponse(wiremockServer, "standard") + setConsignmentReferenceResponse(wiremockServer) + val someDateTime = ZonedDateTime.of(LocalDateTime.of(2022, 3, 10, 1, 0), ZoneId.systemDefault()) + val consignmentStatuses = List( + gcs.ConsignmentStatuses(UUID.randomUUID(), UUID.randomUUID(), DraftMetadataType.id, statusValue, someDateTime, None) + ) + setConsignmentStatusResponse(app.configuration, wiremockServer, consignmentStatuses = consignmentStatuses) + + val pageAsString = contentAsString(additionalMetadataEntryMethodPage) + + playStatus(additionalMetadataEntryMethodPage) mustBe OK + contentType(additionalMetadataEntryMethodPage) mustBe Some("text/html") + pageAsString must include("Results of CSV Checks - Transfer Digital Records - GOV.UK") + pageAsString must include( + s"""

+ | Results of your metadata checks + |

""".stripMargin + ) + pageAsString must include( + s"""
+ |
+ |
+ | Status + |
+ |
+ | Issues found + |
+ |
+ | + |
+ |
+ | Details + |
+ |
+ | Require details message for draftMetadata.validation.details.$fileError + |
+ |
+ | + |
+ |
+ | Action + |
+ |
+ | Require action message for draftMetadata.validation.action.$fileError + |
+ |
+ |
""".stripMargin + ) + } } } } @@ -117,14 +253,17 @@ class DraftMetadataChecksResultsControllerSpec extends FrontEndTestHelper { private def instantiateController( securityComponents: SecurityComponents = getAuthorisedSecurityComponents, keycloakConfiguration: KeycloakConfiguration = getValidStandardUserKeycloakConfiguration, - blockDraftMetadataUpload: Boolean = true + blockDraftMetadataUpload: Boolean = true, + fileError: FileError.FileError = FileError.UNKNOWN ): DraftMetadataChecksResultsController = { when(configuration.get[Boolean]("featureAccessBlock.blockDraftMetadataUpload")).thenReturn(blockDraftMetadataUpload) val applicationConfig: ApplicationConfig = new ApplicationConfig(configuration) val graphQLConfiguration = new GraphQLConfiguration(app.configuration) val consignmentService = new ConsignmentService(graphQLConfiguration) val consignmentStatusService = new ConsignmentStatusService(graphQLConfiguration) + val draftMetaDataService = mock[DraftMetadataService] + when(draftMetaDataService.getErrorTypeFromErrorJson(any[UUID])).thenReturn(Future.successful(fileError)) - new DraftMetadataChecksResultsController(securityComponents, keycloakConfiguration, consignmentService, applicationConfig, consignmentStatusService) + new DraftMetadataChecksResultsController(securityComponents, keycloakConfiguration, consignmentService, applicationConfig, consignmentStatusService, draftMetaDataService) } } diff --git a/test/controllers/DraftMetadataUploadControllerSpec.scala b/test/controllers/DraftMetadataUploadControllerSpec.scala index 26318b0de..41afc759b 100644 --- a/test/controllers/DraftMetadataUploadControllerSpec.scala +++ b/test/controllers/DraftMetadataUploadControllerSpec.scala @@ -14,7 +14,7 @@ import play.api.mvc.{MultipartFormData, Result} import play.api.test.CSRFTokenHelper._ import play.api.test.Helpers.{status => playStatus, _} import play.api.test.{FakeHeaders, FakeRequest} -import services.{ConsignmentService, DraftMetadataService, UploadService} +import services.{ConsignmentService, DraftMetadataService, FileError, UploadService} import software.amazon.awssdk.services.s3.model.PutObjectResponse import testUtils.FrontEndTestHelper @@ -154,6 +154,7 @@ class DraftMetadataUploadControllerSpec extends FrontEndTestHelper { draftMetadataService: DraftMetadataService = mock[DraftMetadataService] ): DraftMetadataUploadController = { when(configuration.get[Boolean]("featureAccessBlock.blockDraftMetadataUpload")).thenReturn(blockDraftMetadataUpload) + when(draftMetadataService.getErrorTypeFromErrorJson(any[UUID])).thenReturn(Future.successful(FileError.NONE)) val applicationConfig: ApplicationConfig = new ApplicationConfig(configuration) val graphQLConfiguration = new GraphQLConfiguration(app.configuration) val consignmentService = new ConsignmentService(graphQLConfiguration) diff --git a/test/services/DownloadServiceSpec.scala b/test/services/DownloadServiceSpec.scala new file mode 100644 index 000000000..097eb83ce --- /dev/null +++ b/test/services/DownloadServiceSpec.scala @@ -0,0 +1,59 @@ +package services + +import configuration.ApplicationConfig +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito._ +import org.scalatest.concurrent.ScalaFutures.convertScalaFuture +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers.convertToAnyShouldWrapper +import org.scalatestplus.mockito.MockitoSugar.mock +import software.amazon.awssdk.core.ResponseBytes +import software.amazon.awssdk.core.async.AsyncResponseTransformer +import software.amazon.awssdk.services.s3.S3AsyncClient +import software.amazon.awssdk.services.s3.model.{GetObjectRequest, GetObjectResponse} + +import java.util.concurrent.CompletableFuture +import scala.concurrent.{ExecutionContext, Future} + +class DownloadServiceSpec extends AnyFlatSpec { + + implicit val ec: ExecutionContext = ExecutionContext.global + + val mockAppConfig: ApplicationConfig = mock[ApplicationConfig] + val s3Endpoint = "https://mock-s3-endpoint.com" + val s3AsyncClient: S3AsyncClient = mock[S3AsyncClient] + when(mockAppConfig.s3Endpoint).thenReturn(s3Endpoint) + + val downloadService = new DownloadService(mockAppConfig) + val bucket = "my-test-bucket" + val key = "test-file.txt" + + "DownloadService" should "download a file using an S3AsyncClient returning the response wrapped in a Scala Future" in { + + val mockResponseBytes = mock[ResponseBytes[GetObjectResponse]] + val mockCompletableFuture = CompletableFuture.completedFuture(mockResponseBytes) + + when(s3AsyncClient.getObject(any[GetObjectRequest], any[AsyncResponseTransformer[GetObjectResponse, ResponseBytes[GetObjectResponse]]])) + .thenReturn(mockCompletableFuture) + + val result: Future[ResponseBytes[GetObjectResponse]] = downloadService.downloadFile(bucket, key, s3AsyncClient) + + result.futureValue shouldBe mockResponseBytes + + } + + "DownloadService" should "pass through exceptions when downloading a file from S3" in { + val mockException = new RuntimeException("S3 error") + val mockFailedFuture = new CompletableFuture[ResponseBytes[GetObjectResponse]]() + + mockFailedFuture.completeExceptionally(mockException) + + when(s3AsyncClient.getObject(any[GetObjectRequest], any[AsyncResponseTransformer[GetObjectResponse, ResponseBytes[GetObjectResponse]]])) + .thenReturn(mockFailedFuture) + + val result = downloadService.downloadFile(bucket, key, s3AsyncClient) + + result.failed.futureValue shouldBe mockException + + } +} diff --git a/test/services/DraftMetadataServiceSpec.scala b/test/services/DraftMetadataServiceSpec.scala index 9386ae8ea..8fbb75b4e 100644 --- a/test/services/DraftMetadataServiceSpec.scala +++ b/test/services/DraftMetadataServiceSpec.scala @@ -1,7 +1,8 @@ package services +import configuration.ApplicationConfig import org.mockito.ArgumentCaptor -import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.{any, anyString} import org.mockito.Mockito.when import org.scalatest.concurrent.ScalaFutures.convertScalaFuture import org.scalatest.matchers.should.Matchers._ @@ -9,10 +10,13 @@ import org.scalatest.wordspec.AnyWordSpec import org.scalatestplus.mockito.MockitoSugar import play.api.libs.ws.{WSClient, WSRequest, WSResponse} import play.api.{ConfigLoader, Configuration} +import software.amazon.awssdk.core.ResponseBytes +import software.amazon.awssdk.services.s3.model.{GetObjectRequest, GetObjectResponse} import java.util.UUID import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, Future} class DraftMetadataServiceSpec extends AnyWordSpec with MockitoSugar { @@ -23,6 +27,8 @@ class DraftMetadataServiceSpec extends AnyWordSpec with MockitoSugar { val wsClient = mock[WSClient] val request = mock[WSRequest] val config = mock[Configuration] + val applicationConfig = mock[ApplicationConfig] + val downloadService = mock[DownloadService] val response = mock[WSResponse] val argumentCaptor: ArgumentCaptor[String] = ArgumentCaptor.forClass(classOf[String]) when(config.get[String](any[String])(any[ConfigLoader[String]])).thenReturn("http://localhost") @@ -30,7 +36,7 @@ class DraftMetadataServiceSpec extends AnyWordSpec with MockitoSugar { when(request.addHttpHeaders(any[(String, String)])).thenReturn(request) when(response.status).thenReturn(200) when(request.post[String]("{}")).thenReturn(Future(response)) - val service = new DraftMetadataService(wsClient, config) + val service = new DraftMetadataService(wsClient, config, applicationConfig, downloadService) val consignmentId = UUID.randomUUID() service.triggerDraftMetadataValidator(consignmentId, uploadFileName, "token").futureValue argumentCaptor.getValue should equal(s"http://localhost/draft-metadata/validate/$consignmentId/$uploadFileName") @@ -41,12 +47,14 @@ class DraftMetadataServiceSpec extends AnyWordSpec with MockitoSugar { val request = mock[WSRequest] val config = mock[Configuration] val response = mock[WSResponse] + val applicationConfig = mock[ApplicationConfig] + val downloadService = mock[DownloadService] when(config.get[String](any[String])(any[ConfigLoader[String]])).thenReturn("http://localhost") when(wsClient.url(any[String])).thenReturn(request) when(request.addHttpHeaders(any[(String, String)])).thenReturn(request) when(response.status).thenReturn(200) when(request.post[String]("{}")).thenReturn(Future(response)) - val service = new DraftMetadataService(wsClient, config) + val service = new DraftMetadataService(wsClient, config, applicationConfig, downloadService) val consignmentId = UUID.randomUUID() val triggerResponse = service.triggerDraftMetadataValidator(consignmentId, uploadFileName, "token").futureValue triggerResponse should equal(true) @@ -57,15 +65,66 @@ class DraftMetadataServiceSpec extends AnyWordSpec with MockitoSugar { val request = mock[WSRequest] val config = mock[Configuration] val response = mock[WSResponse] + val applicationConfig = mock[ApplicationConfig] + val downloadService = mock[DownloadService] when(config.get[String](any[String])(any[ConfigLoader[String]])).thenReturn("http://localhost") when(wsClient.url(any[String])).thenReturn(request) when(request.addHttpHeaders(any[(String, String)])).thenReturn(request) when(response.status).thenReturn(500) when(request.post[String]("{}")).thenReturn(Future(response)) - val service = new DraftMetadataService(wsClient, config) + val service = new DraftMetadataService(wsClient, config, applicationConfig, downloadService) val consignmentId = UUID.randomUUID() val exception = service.triggerDraftMetadataValidator(consignmentId, uploadFileName, "token").failed.futureValue exception.getMessage should equal(s"Call to draft metadata validator failed API has returned a non 200 response for consignment $consignmentId") } } + "getErrorType" should { + val wsClient = mock[WSClient] + val config = mock[Configuration] + val downloadService = mock[DownloadService] + + "get error type from error json file" in { + val errorJson = + """ + |{ + | "consignmentId" : "f82af3bf-b742-454c-9771-bfd6c5eae749", + | "date" : "$today", + | "fileError" : "NONE", + | "validationErrors" : [ + | ] + |} + |""".stripMargin + val mockResponse = GetObjectResponse.builder().build() + val p: ResponseBytes[GetObjectResponse] = ResponseBytes.fromByteArray(mockResponse, errorJson.getBytes()) + when(config.get[String]("draftMetadata.errorFileName")).thenReturn("error.json") + when(config.get[String]("draft_metadata_s3_bucket_name")).thenReturn("bucket") + val applicationConfig: ApplicationConfig = new ApplicationConfig(config) + when(downloadService.downloadFile(anyString, anyString)).thenReturn(Future.successful(p)) + val service = new DraftMetadataService(wsClient, config, applicationConfig, downloadService) + + Await.result(service.getErrorTypeFromErrorJson(UUID.randomUUID()), Duration("1 seconds")) shouldBe FileError.NONE + } + + "get error type will be unspecified if none in json" in { + val errorJson = + """ + |{ + | "consignmentId" : "f82af3bf-b742-454c-9771-bfd6c5eae749", + | "date" : "$today", + | | "validationErrors" : [ + | ] + |} + |""".stripMargin + val mockResponse = GetObjectResponse.builder().build() + val p: ResponseBytes[GetObjectResponse] = ResponseBytes.fromByteArray(mockResponse, errorJson.getBytes()) + when(config.get[String]("draftMetadata.errorFileName")).thenReturn("error.json") + when(config.get[String]("draft_metadata_s3_bucket_name")).thenReturn("bucket") + val applicationConfig: ApplicationConfig = new ApplicationConfig(config) + when(downloadService.downloadFile(anyString, anyString)).thenReturn(Future.successful(p)) + val service = new DraftMetadataService(wsClient, config, applicationConfig, downloadService) + + Await.result(service.getErrorTypeFromErrorJson(UUID.randomUUID()), Duration("1 seconds")) shouldBe FileError.UNKNOWN + } + + } }