From 0f483910cf119cb884fefd2d16b5f28ad5a9f564 Mon Sep 17 00:00:00 2001 From: petekirby-ee Date: Thu, 16 May 2024 11:04:08 +0100 Subject: [PATCH] APIS-7016 Add command and event for Resend Verification email (#494) * APIS-7016 Add new command ResendRequesterEmailVerification handler etc * APIS-7016 Add new command ResendRequesterEmailVerification handler etc * APIS-7016 Add new command ResendRequesterEmailVerification handler etc * APIS-7016 Add new command ResendRequesterEmailVerification handler etc * APIS-7016 Add new command ResendRequesterEmailVerification handler etc * APIS-7016 Add new command ResendRequesterEmailVerification handler etc --- .../controllers/GatekeeperController.scala | 1 + .../services/AuditService.scala | 13 +- .../services/GatekeeperService.scala | 3 +- .../services/commands/CommandHandler.scala | 12 ++ ...esterEmailVerificationCommandHandler.scala | 92 ++++++++++++ .../SubmissionCommandsProcessor.scala | 4 +- .../notifications/NotificationService.scala | 1 + .../VerifyRequesterEmailNotification.scala | 38 +++++ .../connectors/EmailConnectorMockModule.scala | 4 + .../ApplicationCommandDispatcherSpec.scala | 35 +++++ .../services/GatekeeperServiceSpec.scala | 2 +- ...rEmailVerificationCommandHandlerSpec.scala | 133 ++++++++++++++++++ .../NotificationServiceSpec.scala | 25 ++++ .../ApplicationCommandDispatcherUtils.scala | 4 +- 14 files changed, 362 insertions(+), 5 deletions(-) create mode 100644 app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/ResendRequesterEmailVerificationCommandHandler.scala create mode 100644 app/uk/gov/hmrc/thirdpartyapplication/services/notifications/VerifyRequesterEmailNotification.scala create mode 100644 test/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/ResendRequesterEmailVerificationCommandHandlerSpec.scala diff --git a/app/uk/gov/hmrc/thirdpartyapplication/controllers/GatekeeperController.scala b/app/uk/gov/hmrc/thirdpartyapplication/controllers/GatekeeperController.scala index 84e4abd9e..a518732eb 100644 --- a/app/uk/gov/hmrc/thirdpartyapplication/controllers/GatekeeperController.scala +++ b/app/uk/gov/hmrc/thirdpartyapplication/controllers/GatekeeperController.scala @@ -55,6 +55,7 @@ class GatekeeperController @Inject() ( JsErrorResponse(INVALID_STATE_TRANSITION, "Application is not in state 'PENDING_REQUESTER_VERIFICATION'") ) + @deprecated def resendVerification(id: ApplicationId) = requiresAuthentication().async(parse.json) { implicit request => withJsonBody[ResendVerificationRequest] { resendVerificationPayload => diff --git a/app/uk/gov/hmrc/thirdpartyapplication/services/AuditService.scala b/app/uk/gov/hmrc/thirdpartyapplication/services/AuditService.scala index a1af683dc..00d8cacc3 100644 --- a/app/uk/gov/hmrc/thirdpartyapplication/services/AuditService.scala +++ b/app/uk/gov/hmrc/thirdpartyapplication/services/AuditService.scala @@ -95,6 +95,7 @@ class AuditService @Inject() (val auditConnector: AuditConnector, val submission case evt: SandboxApplicationPrivacyPolicyUrlRemoved => auditSandboxApplicationPrivacyPolicyUrlRemoved(app, evt) case evt: SandboxApplicationTermsAndConditionsUrlChanged => auditSandboxApplicationTermsAndConditionsUrlChanged(app, evt) case evt: SandboxApplicationTermsAndConditionsUrlRemoved => auditSandboxApplicationTermsAndConditionsUrlRemoved(app, evt) + case evt: RequesterEmailVerificationResent => auditRequesterEmailVerificationResent(app, evt) case _ => Future.successful(None) } } @@ -228,6 +229,16 @@ class AuditService @Inject() (val auditConnector: AuditConnector, val submission )) .toOption .value + + private def auditRequesterEmailVerificationResent(app: StoredApplication, evt: RequesterEmailVerificationResent)(implicit hc: HeaderCarrier): Future[Option[AuditResult]] = + E.liftF(auditGatekeeperAction( + evt.actor.user, + app, + ApplicationVerificationResent + )) + .toOption + .value + } sealed trait AuditAction { @@ -317,7 +328,7 @@ object AuditAction { val auditType = "ApplicationNameDeclinedByGatekeeper" } - case object ApplicationVerficationResent extends AuditAction { + case object ApplicationVerificationResent extends AuditAction { val name = "verification email has been resent" val auditType = "VerificationEmailResentByGatekeeper" } diff --git a/app/uk/gov/hmrc/thirdpartyapplication/services/GatekeeperService.scala b/app/uk/gov/hmrc/thirdpartyapplication/services/GatekeeperService.scala index 41dc24203..7ef021546 100644 --- a/app/uk/gov/hmrc/thirdpartyapplication/services/GatekeeperService.scala +++ b/app/uk/gov/hmrc/thirdpartyapplication/services/GatekeeperService.scala @@ -88,6 +88,7 @@ class GatekeeperService @Inject() ( } yield history } + @deprecated def resendVerification(applicationId: ApplicationId, gatekeeperUserId: String)(implicit hc: HeaderCarrier): Future[ApplicationStateChange] = { def rejectIfNotPendingVerification(existing: StoredApplication) = { existing.state.requireState(State.PENDING_REQUESTER_VERIFICATION, State.PENDING_REQUESTER_VERIFICATION) @@ -103,7 +104,7 @@ class GatekeeperService @Inject() ( for { app <- fetchApp(applicationId) _ = rejectIfNotPendingVerification(app) - _ = auditService.auditGatekeeperAction(gatekeeperUserId, app, ApplicationVerficationResent) + _ = auditService.auditGatekeeperAction(gatekeeperUserId, app, ApplicationVerificationResent) _ = recoverAll(sendEmails(app)) } yield UpliftApproved diff --git a/app/uk/gov/hmrc/thirdpartyapplication/services/commands/CommandHandler.scala b/app/uk/gov/hmrc/thirdpartyapplication/services/commands/CommandHandler.scala index 9f2dc415f..2c9630ebe 100644 --- a/app/uk/gov/hmrc/thirdpartyapplication/services/commands/CommandHandler.scala +++ b/app/uk/gov/hmrc/thirdpartyapplication/services/commands/CommandHandler.scala @@ -133,6 +133,12 @@ object CommandHandler extends BaseCommandHandler[(StoredApplication, NonEmptyLis GenericFailure("App is not in PENDING_RESPONSIBLE_INDIVIDUAL_VERIFICATION state") ) + def isPendingRequesterVerification(app: StoredApplication) = + cond( + app.isPendingRequesterVerification, + GenericFailure("App is not in PENDING_REQUESTER_VERIFICATION state") + ) + def isInTesting(app: StoredApplication) = cond( app.isInTesting, @@ -201,4 +207,10 @@ object CommandHandler extends BaseCommandHandler[(StoredApplication, NonEmptyLis def appHasLessThanLimitOfSecrets(app: StoredApplication, clientSecretLimit: Int): Validated[Failures, Unit] = cond(app.tokens.production.clientSecrets.size < clientSecretLimit, GenericFailure("Client secret limit has been exceeded")) + + def getVerificationCode(app: StoredApplication): Option[String] = + app.state.verificationCode + + def ensureVerificationCodeDefined(app: StoredApplication) = + mustBeDefined(getVerificationCode(app), "The verificationCode has not been set for this application") } diff --git a/app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/ResendRequesterEmailVerificationCommandHandler.scala b/app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/ResendRequesterEmailVerificationCommandHandler.scala new file mode 100644 index 000000000..a4c3a85ad --- /dev/null +++ b/app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/ResendRequesterEmailVerificationCommandHandler.scala @@ -0,0 +1,92 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.gov.hmrc.thirdpartyapplication.services.commands.submission + +import javax.inject.{Inject, Singleton} +import scala.concurrent.{ExecutionContext, Future} + +import cats.Apply +import cats.data.{NonEmptyList, Validated} +import cats.syntax.validated._ + +import uk.gov.hmrc.apiplatform.modules.common.domain.models.{Actors, LaxEmailAddress} +import uk.gov.hmrc.apiplatform.modules.commands.applications.domain.models.ApplicationCommands.ResendRequesterEmailVerification +import uk.gov.hmrc.apiplatform.modules.commands.applications.domain.models.{CommandFailure, CommandFailures} +import uk.gov.hmrc.apiplatform.modules.events.applications.domain.models.ApplicationEvents._ +import uk.gov.hmrc.apiplatform.modules.events.applications.domain.models._ +import uk.gov.hmrc.apiplatform.modules.submissions.domain.models._ +import uk.gov.hmrc.apiplatform.modules.submissions.services.SubmissionsService +import uk.gov.hmrc.thirdpartyapplication.models.db.StoredApplication +import uk.gov.hmrc.thirdpartyapplication.services.commands.CommandHandler + +@Singleton +class ResendRequesterEmailVerificationCommandHandler @Inject() ( + submissionService: SubmissionsService + )(implicit val ec: ExecutionContext + ) extends CommandHandler { + + import CommandHandler._ + import CommandFailures._ + + private def validate(app: StoredApplication): Future[Validated[Failures, (LaxEmailAddress, String, Submission)]] = { + + def checkSubmission(maybeSubmission: Option[Submission]): Validated[Failures, Submission] = { + lazy val fails: CommandFailure = GenericFailure(s"No submission found for application ${app.id.value}") + maybeSubmission.fold(fails.invalidNel[Submission])(_.validNel[CommandFailure]) + } + + submissionService.fetchLatest(app.id).map { maybeSubmission => + Apply[Validated[Failures, *]].map6( + isStandardNewJourneyApp(app), + isPendingRequesterVerification(app), + ensureRequesterEmailDefined(app), + ensureRequesterNameDefined(app), + ensureVerificationCodeDefined(app), + checkSubmission(maybeSubmission) + ) { case (_, _, requesterEmail, requesterName, _, submission) => (requesterEmail, requesterName, submission) } + } + } + + private def asEvents( + app: StoredApplication, + cmd: ResendRequesterEmailVerification, + submission: Submission, + requesterEmail: LaxEmailAddress, + requesterName: String + ): (RequesterEmailVerificationResent) = { + ( + RequesterEmailVerificationResent( + id = EventId.random, + applicationId = app.id, + eventDateTime = cmd.timestamp, + actor = Actors.GatekeeperUser(cmd.gatekeeperUser), + submissionId = submission.id, + submissionIndex = submission.latestInstance.index, + requestingAdminName = requesterName, + requestingAdminEmail = requesterEmail + ) + ) + } + + def process(app: StoredApplication, cmd: ResendRequesterEmailVerification): AppCmdResultT = { + for { + validated <- E.fromValidatedF(validate(app)) + (requesterEmail, requesterName, submission) = validated + emailResent = asEvents(app, cmd, submission, requesterEmail, requesterName) + } yield (app, NonEmptyList.one(emailResent)) + } +} diff --git a/app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmissionCommandsProcessor.scala b/app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmissionCommandsProcessor.scala index d106da012..a37147d97 100644 --- a/app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmissionCommandsProcessor.scala +++ b/app/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/SubmissionCommandsProcessor.scala @@ -29,7 +29,8 @@ class SubmissionCommandsProcessor @Inject() ( verifyResponsibleIndividualCmdHdlr: VerifyResponsibleIndividualCommandHandler, declineApplicationApprovalRequestCommandHandler: DeclineApplicationApprovalRequestCommandHandler, declineResponsibleIndividualCmdHdlr: DeclineResponsibleIndividualCommandHandler, - declineResponsibleIndividualDidNotVerifyCmdHdlr: DeclineResponsibleIndividualDidNotVerifyCommandHandler + declineResponsibleIndividualDidNotVerifyCmdHdlr: DeclineResponsibleIndividualDidNotVerifyCommandHandler, + resendRequesterEmailVerificationCmdHdlr: ResendRequesterEmailVerificationCommandHandler ) { import CommandHandler._ import ApplicationCommands._ @@ -41,5 +42,6 @@ class SubmissionCommandsProcessor @Inject() ( case cmd: DeclineApplicationApprovalRequest => declineApplicationApprovalRequestCommandHandler.process(app, cmd) case cmd: DeclineResponsibleIndividual => declineResponsibleIndividualCmdHdlr.process(app, cmd) case cmd: DeclineResponsibleIndividualDidNotVerify => declineResponsibleIndividualDidNotVerifyCmdHdlr.process(app, cmd) + case cmd: ResendRequesterEmailVerification => resendRequesterEmailVerificationCmdHdlr.process(app, cmd) } } diff --git a/app/uk/gov/hmrc/thirdpartyapplication/services/notifications/NotificationService.scala b/app/uk/gov/hmrc/thirdpartyapplication/services/notifications/NotificationService.scala index a390320e8..84d235d3c 100644 --- a/app/uk/gov/hmrc/thirdpartyapplication/services/notifications/NotificationService.scala +++ b/app/uk/gov/hmrc/thirdpartyapplication/services/notifications/NotificationService.scala @@ -97,6 +97,7 @@ class NotificationService @Inject() (emailConnector: EmailConnector)(implicit va case evt: TermsOfUsePassed => TermsOfUsePassedNotification.sendAdviceEmail(emailConnector, app, evt) case evt: ProductionCredentialsApplicationDeleted => ProductionCredentialsApplicationDeletedNotification.sendAdviceEmail(emailConnector, app, evt) case evt: ApplicationDeletedByGatekeeper => ApplicationDeletedByGatekeeperNotification.sendAdviceEmail(emailConnector, app, evt) + case evt: RequesterEmailVerificationResent => VerifyRequesterEmailNotification.sendAdviceEmail(emailConnector, app, evt) case _ => Future.successful(HasSucceeded) } } diff --git a/app/uk/gov/hmrc/thirdpartyapplication/services/notifications/VerifyRequesterEmailNotification.scala b/app/uk/gov/hmrc/thirdpartyapplication/services/notifications/VerifyRequesterEmailNotification.scala new file mode 100644 index 000000000..cd7a67058 --- /dev/null +++ b/app/uk/gov/hmrc/thirdpartyapplication/services/notifications/VerifyRequesterEmailNotification.scala @@ -0,0 +1,38 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.gov.hmrc.thirdpartyapplication.services.notifications + +import scala.concurrent.Future + +import uk.gov.hmrc.http.HeaderCarrier + +import uk.gov.hmrc.apiplatform.modules.events.applications.domain.models.ApplicationEvents.RequesterEmailVerificationResent +import uk.gov.hmrc.thirdpartyapplication.connector.EmailConnector +import uk.gov.hmrc.thirdpartyapplication.models.HasSucceeded +import uk.gov.hmrc.thirdpartyapplication.models.db.StoredApplication + +object VerifyRequesterEmailNotification { + + def sendAdviceEmail(emailConnector: EmailConnector, app: StoredApplication, event: RequesterEmailVerificationResent)(implicit hc: HeaderCarrier): Future[HasSucceeded] = { + val verificationCode: String = app.state.verificationCode.getOrElse("") // Note verificationCode has already been validated + emailConnector.sendApplicationApprovedAdminConfirmation( + app.name, + verificationCode, + Set(event.requestingAdminEmail) + ) + } +} diff --git a/test/uk/gov/hmrc/thirdpartyapplication/mocks/connectors/EmailConnectorMockModule.scala b/test/uk/gov/hmrc/thirdpartyapplication/mocks/connectors/EmailConnectorMockModule.scala index 7abb1c641..6960ed904 100644 --- a/test/uk/gov/hmrc/thirdpartyapplication/mocks/connectors/EmailConnectorMockModule.scala +++ b/test/uk/gov/hmrc/thirdpartyapplication/mocks/connectors/EmailConnectorMockModule.scala @@ -76,6 +76,10 @@ trait EmailConnectorMockModule extends MockitoSugar with ArgumentMatchersSugar { def thenReturnSuccess() = { when(aMock.sendApplicationApprovedAdminConfirmation(*, *, *)(*)).thenReturn(successful(HasSucceeded)) } + + def verifyCalledWith(applicationName: String, code: String, recipients: Set[LaxEmailAddress]): Future[HasSucceeded] = { + verify.sendApplicationApprovedAdminConfirmation(eqTo(applicationName), eqTo(code), eqTo(recipients))(*) + } } object SendVerifyResponsibleIndividualNotification { diff --git a/test/uk/gov/hmrc/thirdpartyapplication/services/ApplicationCommandDispatcherSpec.scala b/test/uk/gov/hmrc/thirdpartyapplication/services/ApplicationCommandDispatcherSpec.scala index 41b798923..46f7caf9e 100644 --- a/test/uk/gov/hmrc/thirdpartyapplication/services/ApplicationCommandDispatcherSpec.scala +++ b/test/uk/gov/hmrc/thirdpartyapplication/services/ApplicationCommandDispatcherSpec.scala @@ -92,6 +92,7 @@ class ApplicationCommandDispatcherSpec mockChangeResponsibleIndividualToOtherCommandHandler, mockVerifyResponsibleIndividualCommandHandler, mockDeclineResponsibleIndividualCommandHandler, + mockResendRequesterEmailVerificationCommandHandler, mockDeclineResponsibleIndividualDidNotVerifyCommandHandler, mockChangeSandboxApplicationNameCommandHandler, mockChangeSandboxApplicationDescriptionCommandHandler, @@ -465,6 +466,40 @@ class ApplicationCommandDispatcherSpec } + "ResendRequesterEmailVerification is received" should { + val timestamp = instant + val actor = Actors.GatekeeperUser(gatekeeperUser) + + val cmd = ResendRequesterEmailVerification(actor.user, timestamp) + val evt = ApplicationEvents.RequesterEmailVerificationResent( + EventId.random, + applicationId, + instant, + actor, + SubmissionId(SubmissionId.random.value), + 1, + "adminName", + "anAdminEmail".toLaxEmail + ) + + "call ResendRequesterEmailVerification Handler and relevant common services if application exists" in new Setup { + primeCommonServiceSuccess() + + when(mockResendRequesterEmailVerificationCommandHandler.process(*[StoredApplication], *[ResendRequesterEmailVerification])).thenReturn(E.pure(( + applicationData, + NonEmptyList.one(evt) + ))) + + await(underTest.dispatch(applicationId, cmd, Set.empty).value) + verifyServicesCalledWithEvent(evt) + verifyNoneButGivenCmmandHandlerCalled[ResendRequesterEmailVerificationCommandHandler]() + } + + "bubble up exception when application fetch fails" in new Setup { + testFailure(cmd) + } + } + "DeleteApplicationByCollaborator is received" should { val cmd = DeleteApplicationByCollaborator(UserId.random, reasons, timestamp) diff --git a/test/uk/gov/hmrc/thirdpartyapplication/services/GatekeeperServiceSpec.scala b/test/uk/gov/hmrc/thirdpartyapplication/services/GatekeeperServiceSpec.scala index 2ab1b8725..de5188fc5 100644 --- a/test/uk/gov/hmrc/thirdpartyapplication/services/GatekeeperServiceSpec.scala +++ b/test/uk/gov/hmrc/thirdpartyapplication/services/GatekeeperServiceSpec.scala @@ -217,7 +217,7 @@ class GatekeeperServiceSpec await(underTest.resendVerification(applicationId, gatekeeperUserId)) AuditServiceMock.AuditGatekeeperAction.verifyUserName() shouldBe gatekeeperUserId - AuditServiceMock.AuditGatekeeperAction.verifyAction() shouldBe AuditAction.ApplicationVerficationResent + AuditServiceMock.AuditGatekeeperAction.verifyAction() shouldBe AuditAction.ApplicationVerificationResent } "fail with InvalidStateTransition when the application is not in PENDING_REQUESTER_VERIFICATION state" in new Setup { diff --git a/test/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/ResendRequesterEmailVerificationCommandHandlerSpec.scala b/test/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/ResendRequesterEmailVerificationCommandHandlerSpec.scala new file mode 100644 index 000000000..405cb053d --- /dev/null +++ b/test/uk/gov/hmrc/thirdpartyapplication/services/commands/submission/ResendRequesterEmailVerificationCommandHandlerSpec.scala @@ -0,0 +1,133 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.gov.hmrc.thirdpartyapplication.services.commands.submission + +import scala.concurrent.ExecutionContext.Implicits.global + +import uk.gov.hmrc.http.HeaderCarrier + +import uk.gov.hmrc.apiplatform.modules.common.domain.models.LaxEmailAddress.StringSyntax +import uk.gov.hmrc.apiplatform.modules.common.domain.models.{Actors, UserId} +import uk.gov.hmrc.apiplatform.modules.common.utils.FixedClock +import uk.gov.hmrc.apiplatform.modules.applications.access.domain.models.Access +import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models.{ApplicationState, State} +import uk.gov.hmrc.apiplatform.modules.applications.submissions.domain.models._ +import uk.gov.hmrc.apiplatform.modules.commands.applications.domain.models.ApplicationCommands.ResendRequesterEmailVerification +import uk.gov.hmrc.apiplatform.modules.events.applications.domain.models.ApplicationEvents.RequesterEmailVerificationResent +import uk.gov.hmrc.apiplatform.modules.submissions.SubmissionsTestData +import uk.gov.hmrc.apiplatform.modules.submissions.mocks.SubmissionsServiceMockModule +import uk.gov.hmrc.thirdpartyapplication.domain.models._ +import uk.gov.hmrc.thirdpartyapplication.services.commands.CommandHandlerBaseSpec + +class ResendRequesterEmailVerificationCommandHandlerSpec extends CommandHandlerBaseSpec with SubmissionsTestData { + + trait Setup extends SubmissionsServiceMockModule { + + implicit val hc: HeaderCarrier = HeaderCarrier() + + val submission = aSubmission + val appAdminUserId = UserId.random + val appAdminEmail = "admin@example.com".toLaxEmail + val appAdminName = "Ms Admin" + val gatekeeperUser = "GateKeeperUser" + + val importantSubmissionData = ImportantSubmissionData( + None, + ResponsibleIndividual.build("Bob", "bob@example.com"), + Set.empty, + TermsAndConditionsLocations.InDesktopSoftware, + PrivacyPolicyLocations.InDesktopSoftware, + List.empty + ) + + val app = anApplicationData(applicationId).copy( + state = ApplicationStateExamples.pendingRequesterVerification(appAdminEmail.text, appAdminName, "123456789"), + access = Access.Standard(List.empty, None, None, Set.empty, None, Some(importantSubmissionData)) + ) + + val ts = FixedClock.instant + + val underTest = new ResendRequesterEmailVerificationCommandHandler(SubmissionsServiceMock.aMock) + } + + "process" should { + "create correct event for a valid request with a standard app" in new Setup { + SubmissionsServiceMock.FetchLatest.thenReturn(submission) + + val result = await(underTest.process(app, ResendRequesterEmailVerification(gatekeeperUser, instant)).value).value + + inside(result) { case (app, events) => + events should have size 1 + + inside(events.head) { + case event: RequesterEmailVerificationResent => + event.applicationId shouldBe applicationId + event.eventDateTime shouldBe ts + event.actor shouldBe Actors.GatekeeperUser(gatekeeperUser) + event.submissionIndex shouldBe submission.latestInstance.index + event.submissionId.value shouldBe submission.id.value + event.requestingAdminEmail shouldBe appAdminEmail + event.requestingAdminName shouldBe appAdminName + } + } + } + + "return an error if no submission is found for the application" in new Setup { + SubmissionsServiceMock.FetchLatest.thenReturnNone() + + checkFailsWith(s"No submission found for application $applicationId") { + underTest.process(app, ResendRequesterEmailVerification(gatekeeperUser, instant)) + } + } + + "return an error if the application is non-standard" in new Setup { + SubmissionsServiceMock.FetchLatest.thenReturn(submission) + val nonStandardApp = app.copy(access = Access.Ropc(Set.empty)) + + checkFailsWith("Must be a standard new journey application") { + underTest.process(nonStandardApp, ResendRequesterEmailVerification(gatekeeperUser, instant)) + } + } + + "return an error if the application is old journey" in new Setup { + SubmissionsServiceMock.FetchLatest.thenReturn(submission) + val oldJourneyApp = app.copy(access = Access.Standard(List.empty, None, None, Set.empty, None, None)) + + checkFailsWith("Must be a standard new journey application") { + underTest.process(oldJourneyApp, ResendRequesterEmailVerification(gatekeeperUser, instant)) + } + } + + "return an error if the application has no verification code" in new Setup { + SubmissionsServiceMock.FetchLatest.thenReturn(submission) + val noVerificationCodeApp = app.copy(state = ApplicationState(State.PENDING_REQUESTER_VERIFICATION, Some(appAdminEmail.text), Some(appAdminName), None, instant)) + + checkFailsWith("The verificationCode has not been set for this application") { + underTest.process(noVerificationCodeApp, ResendRequesterEmailVerification(gatekeeperUser, instant)) + } + } + + "return an error if the application is not pending requester verification" in new Setup { + SubmissionsServiceMock.FetchLatest.thenReturn(submission) + val notApprovedApp = app.copy(state = ApplicationStateExamples.pendingGatekeeperApproval("someone@example.com", "Someone")) + + checkFailsWith("App is not in PENDING_REQUESTER_VERIFICATION state", "The verificationCode has not been set for this application") { + underTest.process(notApprovedApp, ResendRequesterEmailVerification(gatekeeperUser, instant)) + } + } + } +} diff --git a/test/uk/gov/hmrc/thirdpartyapplication/services/notifications/NotificationServiceSpec.scala b/test/uk/gov/hmrc/thirdpartyapplication/services/notifications/NotificationServiceSpec.scala index 557311581..e656a441c 100644 --- a/test/uk/gov/hmrc/thirdpartyapplication/services/notifications/NotificationServiceSpec.scala +++ b/test/uk/gov/hmrc/thirdpartyapplication/services/notifications/NotificationServiceSpec.scala @@ -30,6 +30,7 @@ import uk.gov.hmrc.apiplatform.modules.applications.submissions.domain.models.{S import uk.gov.hmrc.apiplatform.modules.approvals.domain.models.ResponsibleIndividualVerificationId import uk.gov.hmrc.apiplatform.modules.events.applications.domain.models._ import uk.gov.hmrc.thirdpartyapplication.ApplicationStateUtil +import uk.gov.hmrc.thirdpartyapplication.domain.models.ApplicationStateExamples import uk.gov.hmrc.thirdpartyapplication.mocks.connectors.EmailConnectorMockModule import uk.gov.hmrc.thirdpartyapplication.models.HasSucceeded import uk.gov.hmrc.thirdpartyapplication.models.db._ @@ -521,5 +522,29 @@ class NotificationServiceSpec result shouldBe List(HasSucceeded) EmailConnectorMock.SendProductionCredentialsRequestExpired.verifyCalledWith(applicationData.name, collaboratorEmails) } + + "when receive a RequesterEmailVerificationResent, call the event handler and return successfully" in new Setup { + EmailConnectorMock.SendApplicationApprovedAdminConfirmation.thenReturnSuccess() + val requesterName = "Bob Fleming" + val requesterEmail = LaxEmailAddress("bob@example.com") + val verificationCode = "123456789" + val app = applicationData.copy( + state = ApplicationStateExamples.pendingRequesterVerification(requesterEmail.text, requesterName, verificationCode) + ) + val event = ApplicationEvents.RequesterEmailVerificationResent( + EventId.random, + app.id, + instant, + gatekeeperActor, + SubmissionId.random, + 1, + requesterName, + requesterEmail + ) + + val result = await(underTest.sendNotifications(app, NonEmptyList.one(event), Set.empty)) + result shouldBe List(HasSucceeded) + EmailConnectorMock.SendApplicationApprovedAdminConfirmation.verifyCalledWith(app.name, verificationCode, Set(requesterEmail)) + } } } diff --git a/test/uk/gov/hmrc/thirdpartyapplication/testutils/services/ApplicationCommandDispatcherUtils.scala b/test/uk/gov/hmrc/thirdpartyapplication/testutils/services/ApplicationCommandDispatcherUtils.scala index 47bd13c3a..ddb1edbcf 100644 --- a/test/uk/gov/hmrc/thirdpartyapplication/testutils/services/ApplicationCommandDispatcherUtils.scala +++ b/test/uk/gov/hmrc/thirdpartyapplication/testutils/services/ApplicationCommandDispatcherUtils.scala @@ -79,6 +79,7 @@ abstract class ApplicationCommandDispatcherUtils extends AsyncHmrcSpec val mockChangeResponsibleIndividualToOtherCommandHandler: ChangeResponsibleIndividualToOtherCommandHandler = mock[ChangeResponsibleIndividualToOtherCommandHandler] val mockVerifyResponsibleIndividualCommandHandler: VerifyResponsibleIndividualCommandHandler = mock[VerifyResponsibleIndividualCommandHandler] val mockDeclineResponsibleIndividualCommandHandler: DeclineResponsibleIndividualCommandHandler = mock[DeclineResponsibleIndividualCommandHandler] + val mockResendRequesterEmailVerificationCommandHandler: ResendRequesterEmailVerificationCommandHandler = mock[ResendRequesterEmailVerificationCommandHandler] val mockDeclineResponsibleIndividualDidNotVerifyCommandHandler: DeclineResponsibleIndividualDidNotVerifyCommandHandler = mock[DeclineResponsibleIndividualDidNotVerifyCommandHandler] @@ -179,7 +180,8 @@ abstract class ApplicationCommandDispatcherUtils extends AsyncHmrcSpec mockVerifyResponsibleIndividualCommandHandler, mockDeclineApplicationApprovalRequestCommandHandler, mockDeclineResponsibleIndividualCommandHandler, - mockDeclineResponsibleIndividualDidNotVerifyCommandHandler + mockDeclineResponsibleIndividualDidNotVerifyCommandHandler, + mockResendRequesterEmailVerificationCommandHandler ) val subscriptionCommandsProcessor = new SubscriptionCommandsProcessor(