Skip to content

Commit

Permalink
Merge pull request #36 from hmrc/API-3842
Browse files Browse the repository at this point in the history
API-3842: job for purging applications
  • Loading branch information
adampridmore authored Jun 28, 2019
2 parents 208e68d + 4df906e commit 9d5c1f2
Show file tree
Hide file tree
Showing 4 changed files with 249 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import uk.gov.hmrc.play.config.ServicesConfig
import uk.gov.hmrc.thirdpartyapplication.connector._
import uk.gov.hmrc.thirdpartyapplication.controllers.{ApplicationControllerConfig, DocumentationConfig}
import uk.gov.hmrc.thirdpartyapplication.models.TrustedApplicationsConfig
import uk.gov.hmrc.thirdpartyapplication.scheduled.{JobConfig, RefreshSubscriptionsJobConfig, SetLastAccessedDateJobConfig, UpliftVerificationExpiryJobConfig}
import uk.gov.hmrc.thirdpartyapplication.scheduled.{JobConfig, PurgeApplicationsJobConfig, RefreshSubscriptionsJobConfig, SetLastAccessedDateJobConfig, UpliftVerificationExpiryJobConfig}
import uk.gov.hmrc.thirdpartyapplication.services.CredentialConfig

import scala.concurrent.duration.{Duration, FiniteDuration}
Expand All @@ -40,6 +40,7 @@ class ConfigurationModule extends Module {
bind[RefreshSubscriptionsJobConfig].toProvider[RefreshSubscriptionsJobConfigProvider],
bind[UpliftVerificationExpiryJobConfig].toProvider[UpliftVerificationExpiryJobConfigProvider],
bind[SetLastAccessedDateJobConfig].toProvider[SetLastAccessDateJobConfigProvider],
bind[PurgeApplicationsJobConfig].toProvider[PurgeApplicationsJobConfigProvider],
bind[ApiDefinitionConfig].toProvider[ApiDefinitionConfigProvider],
bind[ApiSubscriptionFieldsConfig].toProvider[ApiSubscriptionFieldsConfigProvider],
bind[ApiStorageConfig].toProvider[ApiStorageConfigProvider],
Expand Down Expand Up @@ -122,6 +123,21 @@ class SetLastAccessDateJobConfigProvider @Inject()(val runModeConfiguration: Con
}
}

@Singleton
class PurgeApplicationsJobConfigProvider @Inject()(val runModeConfiguration: Configuration, environment: Environment)
extends Provider[PurgeApplicationsJobConfig] with ServicesConfig {

override protected def mode = environment.mode

override def get() = {

val jobConfig = runModeConfiguration.underlying.as[Option[JobConfig]](s"$env.purgeApplicationsJob")
.getOrElse(JobConfig(FiniteDuration(60, SECONDS), FiniteDuration(24, HOURS), enabled = true)) // scalastyle:off magic.number

PurgeApplicationsJobConfig(jobConfig.initialDelay, jobConfig.interval, jobConfig.enabled)
}
}

@Singleton
class ApiDefinitionConfigProvider @Inject()(val runModeConfiguration: Configuration, environment: Environment)
extends Provider[ApiDefinitionConfig] with ServicesConfig {
Expand Down
12 changes: 10 additions & 2 deletions app/uk/gov/hmrc/thirdpartyapplication/config/Scheduler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import com.google.inject.AbstractModule
import javax.inject.{Inject, Singleton}
import play.api.Application
import uk.gov.hmrc.play.scheduling.{ExclusiveScheduledJob, RunningOfScheduledJobs}
import uk.gov.hmrc.thirdpartyapplication.scheduled._
import uk.gov.hmrc.thirdpartyapplication.scheduled.{SetLastAccessedDateJobConfig, _}

class SchedulerModule extends AbstractModule {
override def configure(): Unit = {
Expand All @@ -35,6 +35,8 @@ class Scheduler @Inject()(upliftVerificationExpiryJobConfig: UpliftVerificationE
refreshSubscriptionsScheduledJob: RefreshSubscriptionsScheduledJob,
setLastAccessedDateJobConfig: SetLastAccessedDateJobConfig,
setLastAccessedDateJob: SetLastAccessedDateJob,
purgeApplicationsJobConfig: PurgeApplicationsJobConfig,
purgeApplicationsJob: PurgeApplicationsJob,
app: Application) extends RunningOfScheduledJobs {

override val scheduledJobs: Seq[ExclusiveScheduledJob] = {
Expand All @@ -57,7 +59,13 @@ class Scheduler @Inject()(upliftVerificationExpiryJobConfig: UpliftVerificationE
Seq.empty
}

upliftJob ++ refreshJob ++ accessDateJob
val purgeAppsJob = if (purgeApplicationsJobConfig.enabled) {
Seq(purgeApplicationsJob)
} else {
Seq.empty
}

upliftJob ++ refreshJob ++ accessDateJob ++ purgeAppsJob

}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Copyright 2019 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.scheduled

import java.util.UUID

import javax.inject.Inject
import org.joda.time.Duration
import play.api.Logger
import play.modules.reactivemongo.ReactiveMongoComponent
import uk.gov.hmrc.lock.{LockKeeper, LockRepository}
import uk.gov.hmrc.thirdpartyapplication.models.{APIIdentifier, HasSucceeded}
import uk.gov.hmrc.thirdpartyapplication.repository.{ApplicationRepository, StateHistoryRepository, SubscriptionRepository}

import scala.collection.Seq
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future.{failed, sequence}
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ExecutionContext, Future}

class PurgeApplicationsJob @Inject()(override val lockKeeper: PurgeApplicationsJobLockKeeper,
jobConfig: PurgeApplicationsJobConfig,
applicationRepository: ApplicationRepository,
stateHistoryRepository: StateHistoryRepository,
subscriptionRepository: SubscriptionRepository) extends ScheduledMongoJob {

override def name: String = "PurgeApplicationsJob"

override def initialDelay: FiniteDuration = jobConfig.initialDelay

override def interval: FiniteDuration = jobConfig.interval

override def runJob(implicit ec: ExecutionContext): Future[RunningOfJobSuccessful] = {
Logger.info("Starting PurgeApplicationsJob")
val applicationIds: Seq[UUID] = Seq(
UUID.fromString("a9633b5b-aae9-4419-8aa1-6317832dc580"),
UUID.fromString("73d33f9f-6e42-4a22-ae1e-5a05ba2be22d")
)

purgeApplications(applicationIds) map { _ =>
Logger.info(s"Purged applications: $applicationIds")
RunningOfJobSuccessful
} recoverWith {
case e: Throwable => failed(RunningOfJobFailed(name, e))
}
}

private def purgeApplications(applicationIds: Seq[UUID]): Future[Seq[RunningOfJobSuccessful]] = {
sequence {
applicationIds map { applicationId =>
for {
_ <- applicationRepository.delete(applicationId)
_ <- stateHistoryRepository.deleteByApplicationId(applicationId)
subscriptions <- subscriptionRepository.getSubscriptions(applicationId)
_ <- deleteSubscriptions(applicationId, subscriptions)
} yield RunningOfJobSuccessful
}
}
}

private def deleteSubscriptions(applicationId: UUID, subscriptions: Seq[APIIdentifier]): Future[Seq[HasSucceeded]] = {
sequence(subscriptions.map(sub => subscriptionRepository.remove(applicationId, sub)))
}
}

class PurgeApplicationsJobLockKeeper @Inject()(mongo: ReactiveMongoComponent) extends LockKeeper {
override def repo: LockRepository = new LockRepository()(mongo.mongoConnector.db)

override def lockId: String = "PurgeApplicationsJobConfig"

override val forceLockReleaseAfter: Duration = Duration.standardMinutes(10) //scalastyle:ignore magic.number
}

case class PurgeApplicationsJobConfig(initialDelay: FiniteDuration, interval: FiniteDuration, enabled: Boolean)
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Copyright 2019 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 unit.uk.gov.hmrc.thirdpartyapplication.scheduled

import java.util.UUID
import java.util.concurrent.TimeUnit.{HOURS, SECONDS}

import org.joda.time.Duration
import org.mockito.Matchers.any
import org.mockito.Mockito._
import org.scalatest.mockito.MockitoSugar
import play.modules.reactivemongo.ReactiveMongoComponent
import uk.gov.hmrc.lock.LockRepository
import uk.gov.hmrc.mongo.{MongoConnector, MongoSpecSupport}
import uk.gov.hmrc.play.test.UnitSpec
import uk.gov.hmrc.thirdpartyapplication.models.{APIIdentifier, HasSucceeded}
import uk.gov.hmrc.thirdpartyapplication.repository.{ApplicationRepository, StateHistoryRepository, SubscriptionRepository}
import uk.gov.hmrc.thirdpartyapplication.scheduled.{PurgeApplicationsJob, PurgeApplicationsJobConfig, PurgeApplicationsJobLockKeeper}

import scala.collection.immutable.ListMap
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future.{failed, successful}
import scala.concurrent.duration.FiniteDuration
import scala.concurrent.{ExecutionContext, Future}

class PurgeApplicationsJobSpec extends UnitSpec with MockitoSugar with MongoSpecSupport {

private val reactiveMongoComponent = new ReactiveMongoComponent {
override def mongoConnector: MongoConnector = mongoConnectorForTest
}

trait Setup {
val expectedApplications: ListMap[UUID, Seq[APIIdentifier]] = ListMap(
UUID.fromString("a9633b5b-aae9-4419-8aa1-6317832dc580") -> Seq(APIIdentifier("hello", "1.0"), APIIdentifier("hello", "2.0")),
UUID.fromString("73d33f9f-6e42-4a22-ae1e-5a05ba2be22d") -> Seq(APIIdentifier("example", "1.0"), APIIdentifier("example", "2.0"))
)
val expectedSubscriptions: Seq[Seq[APIIdentifier]] = expectedApplications.values.toSeq

val mockApplicationRepository: ApplicationRepository = mock[ApplicationRepository]
val mockStateHistoryRepository: StateHistoryRepository = mock[StateHistoryRepository]
val mockSubscriptionRepository: SubscriptionRepository = mock[SubscriptionRepository]

when(mockApplicationRepository.delete(any[UUID])).thenReturn(successful(HasSucceeded))
when(mockStateHistoryRepository.deleteByApplicationId(any[UUID])).thenReturn(successful(HasSucceeded))
when(mockSubscriptionRepository.getSubscriptions(any[UUID]))
.thenReturn(successful(expectedSubscriptions.head))
.thenReturn(successful(expectedSubscriptions(1)))
when(mockSubscriptionRepository.remove(any[UUID], any[APIIdentifier])).thenReturn(successful(HasSucceeded))

val lockKeeperSuccess: () => Boolean = () => true

val mockLockKeeper: PurgeApplicationsJobLockKeeper = new PurgeApplicationsJobLockKeeper(reactiveMongoComponent) {
override def lockId: String = "testLock"

override def repo: LockRepository = mock[LockRepository]

override val forceLockReleaseAfter: Duration = Duration.standardMinutes(5) // scalastyle:off magic.number

override def tryLock[T](body: => Future[T])(implicit ec: ExecutionContext): Future[Option[T]] =
if (lockKeeperSuccess()) body.map(value => successful(Some(value)))
else successful(None)
}

val initialDelay = FiniteDuration(60, SECONDS) // scalastyle:off magic.number
val interval = FiniteDuration(24, HOURS) // scalastyle:off magic.number
val config = PurgeApplicationsJobConfig(initialDelay, interval, enabled = true)

val underTest = new PurgeApplicationsJob(mockLockKeeper, config, mockApplicationRepository, mockStateHistoryRepository, mockSubscriptionRepository)
}

"PurgeApplicationsJob" should {
"purge the applications" in new Setup {
val result: underTest.Result = await(underTest.execute)

result.message shouldBe "PurgeApplicationsJob Job ran successfully."
expectedApplications.foreach { app =>
verify(mockApplicationRepository, times(1)).delete(app._1)
verify(mockStateHistoryRepository, times(1)).deleteByApplicationId(app._1)
app._2.foreach { sub =>
verify(mockSubscriptionRepository).remove(app._1, sub)
}
}
verifyNoMoreInteractions(mockApplicationRepository)
verifyNoMoreInteractions(mockStateHistoryRepository)
}

"fail gracefully when it fails to delete an application" in new Setup {
val underlyingErrorMessage: String = "Something bad happened"
val expectedErrorMessage =
s"The execution of scheduled job PurgeApplicationsJob failed with error '$underlyingErrorMessage'. The next execution of the job will do retry."
when(mockApplicationRepository.delete(any[UUID])).thenReturn(failed(new RuntimeException(underlyingErrorMessage)))

val result: underTest.Result = await(underTest.execute)

result.message shouldBe expectedErrorMessage
}

"fail gracefully when it fails to delete state history" in new Setup {
val underlyingErrorMessage: String = "Something bad happened"
val expectedErrorMessage =
s"The execution of scheduled job PurgeApplicationsJob failed with error '$underlyingErrorMessage'. The next execution of the job will do retry."
when(mockStateHistoryRepository.deleteByApplicationId(any[UUID])).thenReturn(failed(new RuntimeException(underlyingErrorMessage)))

val result: underTest.Result = await(underTest.execute)

result.message shouldBe expectedErrorMessage
}

"fail gracefully when it fails to delete a subscription" in new Setup {
val underlyingErrorMessage: String = "Something bad happened"
val expectedErrorMessage =
s"The execution of scheduled job PurgeApplicationsJob failed with error '$underlyingErrorMessage'. The next execution of the job will do retry."
when(mockSubscriptionRepository.remove(any[UUID], any[APIIdentifier])).thenReturn(failed(new RuntimeException(underlyingErrorMessage)))

val result: underTest.Result = await(underTest.execute)

result.message shouldBe expectedErrorMessage
}
}
}

0 comments on commit 9d5c1f2

Please sign in to comment.