diff --git a/app/uk/gov/hmrc/thirdpartyapplication/models/Application.scala b/app/uk/gov/hmrc/thirdpartyapplication/models/Application.scala index 7f09a0814..2af771c8a 100644 --- a/app/uk/gov/hmrc/thirdpartyapplication/models/Application.scala +++ b/app/uk/gov/hmrc/thirdpartyapplication/models/Application.scala @@ -81,7 +81,7 @@ object Application { data.collaborators, data.createdOn, data.lastAccess, - GrantLength.apply(data.grantLength).getOrElse(GrantLength.EIGHTEEN_MONTHS), + GrantLength.apply(data.refreshTokensAvailableFor).getOrElse(GrantLength.EIGHTEEN_MONTHS), data.tokens.production.lastAccessTokenUsage, redirectUris(data), termsAndConditionsUrl(data), diff --git a/app/uk/gov/hmrc/thirdpartyapplication/models/ExtendedApplicationResponse.scala b/app/uk/gov/hmrc/thirdpartyapplication/models/ExtendedApplicationResponse.scala index 8b3f66ba1..8db8d098d 100644 --- a/app/uk/gov/hmrc/thirdpartyapplication/models/ExtendedApplicationResponse.scala +++ b/app/uk/gov/hmrc/thirdpartyapplication/models/ExtendedApplicationResponse.scala @@ -61,7 +61,7 @@ object ExtendedApplicationResponse { data.collaborators, data.createdOn, data.lastAccess, - GrantLength.apply(data.grantLength).getOrElse(GrantLength.EIGHTEEN_MONTHS), + GrantLength.apply(data.refreshTokensAvailableFor).getOrElse(GrantLength.EIGHTEEN_MONTHS), Application.redirectUris(data), Application.termsAndConditionsUrl(data), Application.privacyPolicyUrl(data), diff --git a/app/uk/gov/hmrc/thirdpartyapplication/models/db/StoredApplication.scala b/app/uk/gov/hmrc/thirdpartyapplication/models/db/StoredApplication.scala index e6a319654..32317fb22 100644 --- a/app/uk/gov/hmrc/thirdpartyapplication/models/db/StoredApplication.scala +++ b/app/uk/gov/hmrc/thirdpartyapplication/models/db/StoredApplication.scala @@ -16,7 +16,7 @@ package uk.gov.hmrc.thirdpartyapplication.models.db -import java.time.Instant +import java.time.{Instant, Period} import com.typesafe.config.ConfigFactory @@ -39,7 +39,7 @@ case class StoredApplication( access: Access = Access.Standard(), createdOn: Instant, lastAccess: Option[Instant], - grantLength: Int = grantLengthConfig, + refreshTokensAvailableFor: Period = Period.ofDays(grantLengthConfig), rateLimitTier: Option[RateLimitTier] = Some(RateLimitTier.BRONZE), environment: String = Environment.PRODUCTION.toString, checkInformation: Option[CheckInformation] = None, diff --git a/app/uk/gov/hmrc/thirdpartyapplication/repository/ApplicationRepository.scala b/app/uk/gov/hmrc/thirdpartyapplication/repository/ApplicationRepository.scala index 3ac482149..ff7541e48 100644 --- a/app/uk/gov/hmrc/thirdpartyapplication/repository/ApplicationRepository.scala +++ b/app/uk/gov/hmrc/thirdpartyapplication/repository/ApplicationRepository.scala @@ -16,11 +16,12 @@ package uk.gov.hmrc.thirdpartyapplication.repository -import java.time.Instant +import java.time.{Instant, Period} import javax.inject.{Inject, Singleton} import scala.concurrent.{ExecutionContext, Future} import com.mongodb.client.model.{FindOneAndUpdateOptions, ReturnDocument} +import com.typesafe.config.ConfigFactory import org.bson.BsonValue import org.mongodb.scala.bson.conversions.Bson import org.mongodb.scala.bson.{BsonArray, BsonInt32, BsonString, Document} @@ -105,8 +106,6 @@ object ApplicationRepository { implicit val formatApplicationState: OFormat[ApplicationState] = Json.format[ApplicationState] implicit val formatApplicationTokens: OFormat[ApplicationTokens] = Json.format[ApplicationTokens] - import uk.gov.hmrc.thirdpartyapplication.models.db.StoredApplication.grantLengthConfig - // Non-standard format compared to companion object val ipAllowlistReads: Reads[IpAllowlist] = ( ((JsPath \ "required").read[Boolean] or Reads.pure(false)) and @@ -114,6 +113,9 @@ object ApplicationRepository { )(IpAllowlist.apply _) implicit val formatIpAllowlist: OFormat[IpAllowlist] = OFormat(ipAllowlistReads, Json.writes[IpAllowlist]) + def periodFromInt(i: Int): Period = (GrantLength.apply(i).getOrElse(GrantLength.EIGHTEEN_MONTHS)).period + val grantLengthConfig = ConfigFactory.load().getInt("grantLengthInDays") + // Non-standard format compared to companion object val readStoredApplication: Reads[StoredApplication] = ( (JsPath \ "id").read[ApplicationId] and @@ -127,7 +129,9 @@ object ApplicationRepository { (JsPath \ "access").read[Access] and (JsPath \ "createdOn").read[Instant] and (JsPath \ "lastAccess").readNullable[Instant] and - ((JsPath \ "grantLength").read[Int] or Reads.pure(grantLengthConfig)) and + (((JsPath \ "refreshTokensAvailableFor").read[Period] + .orElse((JsPath \ "grantLength").read[Int].map(periodFromInt(_)))) + or Reads.pure(periodFromInt(grantLengthConfig))) and (JsPath \ "rateLimitTier").readNullable[RateLimitTier] and (JsPath \ "environment").read[String] and (JsPath \ "checkInformation").readNullable[CheckInformation] and @@ -247,8 +251,8 @@ class ApplicationRepository @Inject() (mongo: MongoComponent, val metrics: Metri def updateApplicationIpAllowlist(applicationId: ApplicationId, ipAllowlist: IpAllowlist): Future[StoredApplication] = updateApplication(applicationId, Updates.set("ipAllowlist", Codecs.toBson(ipAllowlist))) - def updateApplicationGrantLength(applicationId: ApplicationId, grantLength: Int): Future[StoredApplication] = - updateApplication(applicationId, Updates.set("grantLength", grantLength)) + def updateApplicationGrantLength(applicationId: ApplicationId, refreshTokensAvailableFor: Period): Future[StoredApplication] = + updateApplication(applicationId, Updates.set("refreshTokensAvailableFor", Codecs.toBson(refreshTokensAvailableFor))) def addApplicationTermsOfUseAcceptance(applicationId: ApplicationId, acceptance: TermsOfUseAcceptance): Future[StoredApplication] = updateApplication(applicationId, Updates.push("access.importantSubmissionData.termsOfUseAcceptances", Codecs.toBson(acceptance))) @@ -685,6 +689,8 @@ class ApplicationRepository @Inject() (mongo: MongoComponent, val metrics: Metri "access", "createdOn", "lastAccess", + "grantLength", + "refreshTokensAvailableFor", "rateLimitTier", "environment", "allowAutoDelete" diff --git a/app/uk/gov/hmrc/thirdpartyapplication/services/commands/grantlength/ChangeGrantLengthCommandHandler.scala b/app/uk/gov/hmrc/thirdpartyapplication/services/commands/grantlength/ChangeGrantLengthCommandHandler.scala index cbe9af56e..be034787f 100644 --- a/app/uk/gov/hmrc/thirdpartyapplication/services/commands/grantlength/ChangeGrantLengthCommandHandler.scala +++ b/app/uk/gov/hmrc/thirdpartyapplication/services/commands/grantlength/ChangeGrantLengthCommandHandler.scala @@ -16,6 +16,7 @@ package uk.gov.hmrc.thirdpartyapplication.services.commands.grantlength +import java.time.Period import javax.inject.{Inject, Singleton} import scala.concurrent.ExecutionContext @@ -40,8 +41,17 @@ class ChangeGrantLengthCommandHandler @Inject() ( import CommandHandler._ private def validate(app: StoredApplication, cmd: ChangeGrantLength): Validated[Failures, Unit] = { + def printRefreshTokensAvailableFor(refreshTokensAvailableFor: Period): String = { + val refreshTokensAvailableForDays = refreshTokensAvailableFor.getDays + val grantLength = if (refreshTokensAvailableForDays > 0) s"$refreshTokensAvailableForDays days" else "4 hours and no refresh tokens" + s"Grant length is already $grantLength" + } + Apply[Validated[Failures, *]].map( - cond((cmd.grantLength.period.getDays != app.grantLength), CommandFailures.GenericFailure(s"Grant length is already ${app.grantLength} days")) + cond( + (cmd.grantLength.period != app.refreshTokensAvailableFor), + CommandFailures.GenericFailure(printRefreshTokensAvailableFor(app.refreshTokensAvailableFor)) + ) ) { case _ => () } } @@ -52,7 +62,7 @@ class ChangeGrantLengthCommandHandler @Inject() ( applicationId = app.id, eventDateTime = cmd.timestamp, actor = Actors.GatekeeperUser(cmd.gatekeeperUser), - oldGrantLengthInDays = app.grantLength, + oldGrantLengthInDays = app.refreshTokensAvailableFor.getDays, newGrantLengthInDays = cmd.grantLength.period.getDays ) ) @@ -62,7 +72,7 @@ class ChangeGrantLengthCommandHandler @Inject() ( for { valid <- E.fromEither(validate(app, cmd).toEither) - savedApp <- E.liftF(applicationRepository.updateApplicationGrantLength(app.id, cmd.grantLength.period.getDays)) + savedApp <- E.liftF(applicationRepository.updateApplicationGrantLength(app.id, cmd.grantLength.period)) events = asEvents(app, cmd) } yield (savedApp, events) } diff --git a/build.sbt b/build.sbt index 738221daf..daf2329a4 100644 --- a/build.sbt +++ b/build.sbt @@ -69,6 +69,6 @@ commands ++= Seq( Command.command("fixAll") { state => "scalafixAll" :: "it/scalafixAll" :: state }, Command.command("testAll") { state => "test" :: "it/test" :: state }, Command.command("run-all-tests") { state => "testAll" :: state }, - Command.command("clean-and-test") { state => "clean" :: "compile" :: "run-all-tests" :: state }, - Command.command("pre-commit") { state => "clean" :: "scalafmtAll" :: "scalafixAll" :: "coverage" :: "run-all-tests" :: "coverageOff" :: "coverageAggregate" :: state } + Command.command("clean-and-test") { state => "cleanAll" :: "compile" :: "run-all-tests" :: state }, + Command.command("pre-commit") { state => "cleanAll" :: "fmtAll" :: "fixAll" :: "coverage" :: "testAll" :: "coverageOff" :: "coverageAggregate" :: state } ) diff --git a/it/test/uk/gov/hmrc/thirdpartyapplication/component/ThirdPartyApplicationComponentISpec.scala b/it/test/uk/gov/hmrc/thirdpartyapplication/component/ThirdPartyApplicationComponentISpec.scala index 4c1945c5b..32d21f17e 100644 --- a/it/test/uk/gov/hmrc/thirdpartyapplication/component/ThirdPartyApplicationComponentISpec.scala +++ b/it/test/uk/gov/hmrc/thirdpartyapplication/component/ThirdPartyApplicationComponentISpec.scala @@ -16,20 +16,26 @@ package uk.gov.hmrc.thirdpartyapplication.component +import java.util.UUID +import scala.concurrent.Await.{ready, result} +import scala.util.Random + import org.scalatest.Inside +import scalaj.http.{Http, HttpResponse} + import play.api.http.HeaderNames.AUTHORIZATION import play.api.http.Status._ import play.api.inject.bind import play.api.inject.guice.GuiceApplicationBuilder import play.api.libs.json.{Json, OWrites} -import scalaj.http.{Http, HttpResponse} + +import uk.gov.hmrc.apiplatform.modules.common.domain.models.LaxEmailAddress.StringSyntax +import uk.gov.hmrc.apiplatform.modules.common.domain.models._ +import uk.gov.hmrc.apiplatform.modules.common.utils.FixedClock import uk.gov.hmrc.apiplatform.modules.apis.domain.models.ApiIdentifierSyntax._ import uk.gov.hmrc.apiplatform.modules.applications.access.domain.models._ import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models._ import uk.gov.hmrc.apiplatform.modules.commands.applications.domain.models.{ApplicationCommand, ApplicationCommands} -import uk.gov.hmrc.apiplatform.modules.common.domain.models.LaxEmailAddress.StringSyntax -import uk.gov.hmrc.apiplatform.modules.common.domain.models._ -import uk.gov.hmrc.apiplatform.modules.common.utils.FixedClock import uk.gov.hmrc.thirdpartyapplication.config.SchedulerModule import uk.gov.hmrc.thirdpartyapplication.controllers.ApplicationCommandController._ import uk.gov.hmrc.thirdpartyapplication.domain.models._ @@ -38,10 +44,6 @@ import uk.gov.hmrc.thirdpartyapplication.models._ import uk.gov.hmrc.thirdpartyapplication.repository.{ApplicationRepository, SubscriptionRepository} import uk.gov.hmrc.thirdpartyapplication.util.{CollaboratorTestData, CredentialGenerator} -import java.util.UUID -import scala.concurrent.Await.{ready, result} -import scala.util.Random - class DummyCredentialGenerator extends CredentialGenerator { override def generate() = "a" * 10 } diff --git a/it/test/uk/gov/hmrc/thirdpartyapplication/repository/ApplicationRepositoryISpec.scala b/it/test/uk/gov/hmrc/thirdpartyapplication/repository/ApplicationRepositoryISpec.scala index 02a9d1d3c..c42cfe173 100644 --- a/it/test/uk/gov/hmrc/thirdpartyapplication/repository/ApplicationRepositoryISpec.scala +++ b/it/test/uk/gov/hmrc/thirdpartyapplication/repository/ApplicationRepositoryISpec.scala @@ -16,33 +16,35 @@ package uk.gov.hmrc.thirdpartyapplication.repository +import java.time.{Clock, Duration, Instant, Period} +import scala.util.Random.nextString + import org.mockito.MockitoSugar.{mock, times, verify, verifyNoMoreInteractions} import org.mongodb.scala.model.{Filters, Updates} import org.scalatest.BeforeAndAfterEach + import play.api.inject.bind import play.api.inject.guice.GuiceApplicationBuilder import play.api.libs.json._ +import uk.gov.hmrc.mongo.play.json.Codecs +import uk.gov.hmrc.mongo.test.CleanMongoCollectionSupport +import uk.gov.hmrc.utils.ServerBaseISpec + +import uk.gov.hmrc.apiplatform.modules.common.domain.models.LaxEmailAddress.StringSyntax +import uk.gov.hmrc.apiplatform.modules.common.domain.models._ +import uk.gov.hmrc.apiplatform.modules.common.utils.FixedClock import uk.gov.hmrc.apiplatform.modules.apis.domain.models.ApiIdentifierSyntax._ import uk.gov.hmrc.apiplatform.modules.applications.access.domain.models._ import uk.gov.hmrc.apiplatform.modules.applications.common.domain.models.FullName import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models._ import uk.gov.hmrc.apiplatform.modules.applications.submissions.domain.models._ -import uk.gov.hmrc.apiplatform.modules.common.domain.models.LaxEmailAddress.StringSyntax -import uk.gov.hmrc.apiplatform.modules.common.domain.models._ -import uk.gov.hmrc.apiplatform.modules.common.utils.FixedClock import uk.gov.hmrc.apiplatform.modules.submissions.SubmissionsTestData -import uk.gov.hmrc.mongo.play.json.Codecs -import uk.gov.hmrc.mongo.test.CleanMongoCollectionSupport import uk.gov.hmrc.thirdpartyapplication.ApplicationStateUtil import uk.gov.hmrc.thirdpartyapplication.config.SchedulerModule import uk.gov.hmrc.thirdpartyapplication.domain.models._ import uk.gov.hmrc.thirdpartyapplication.models.db._ import uk.gov.hmrc.thirdpartyapplication.models.{StandardAccess => _, _} import uk.gov.hmrc.thirdpartyapplication.util.{ApplicationTestData, JavaDateTimeTestUtils, MetricsHelper} -import uk.gov.hmrc.utils.ServerBaseISpec - -import java.time.{Clock, Duration, Instant} -import scala.util.Random.nextString object ApplicationRepositoryISpecExample extends ServerBaseISpec with FixedClock { val clientId = ClientId.random @@ -74,7 +76,7 @@ object ApplicationRepositoryISpecExample extends ServerBaseISpec with FixedClock ), instant, None, - 123, + GrantLength.EIGHTEEN_MONTHS.period, Some(RateLimitTier.BRONZE), "PRODUCTION", Some(CheckInformation( @@ -88,16 +90,16 @@ object ApplicationRepositoryISpecExample extends ServerBaseISpec with FixedClock ) def json(withInstance: Boolean) = Json.obj( - "id" -> JsString(appId.toString()), - "name" -> JsString("AppName"), - "normalisedName" -> JsString("appname"), - "collaborators" -> JsArray(Seq(Json.obj( + "id" -> JsString(appId.toString()), + "name" -> JsString("AppName"), + "normalisedName" -> JsString("appname"), + "collaborators" -> JsArray(Seq(Json.obj( "userId" -> JsString(userId.toString()), "emailAddress" -> "bob@example.com", "role" -> "ADMINISTRATOR" ))), - "wso2ApplicationName" -> JsString("wso2"), - "tokens" -> Json.obj( + "wso2ApplicationName" -> JsString("wso2"), + "tokens" -> Json.obj( "production" -> Json.obj( "clientId" -> JsString(clientId.toString()), "accessToken" -> JsString("accessABC"), @@ -109,11 +111,11 @@ object ApplicationRepositoryISpecExample extends ServerBaseISpec with FixedClock ))) ) ), - "state" -> Json.obj( + "state" -> Json.obj( "name" -> JsString("TESTING"), "updatedOn" -> MongoJavatimeHelper.asJsValue(instant) ), - "access" -> Json.obj( + "access" -> Json.obj( "redirectUris" -> JsArray(Seq()), "overrides" -> JsArray(Seq()), "importantSubmissionData" -> Json.obj( @@ -149,11 +151,11 @@ object ApplicationRepositoryISpecExample extends ServerBaseISpec with FixedClock ), "accessType" -> JsString("STANDARD") ), - "createdOn" -> MongoJavatimeHelper.asJsValue(instant), - "grantLength" -> JsNumber(123), - "rateLimitTier" -> JsString("BRONZE"), - "environment" -> JsString("PRODUCTION"), - "checkInformation" -> Json.obj( + "createdOn" -> MongoJavatimeHelper.asJsValue(instant), + "refreshTokensAvailableFor" -> GrantLength.EIGHTEEN_MONTHS.period, + "rateLimitTier" -> JsString("BRONZE"), + "environment" -> JsString("PRODUCTION"), + "checkInformation" -> Json.obj( "contactDetails" -> Json.obj( "fullname" -> JsString("Contact"), "email" -> JsString("contact@example.com"), @@ -171,9 +173,9 @@ object ApplicationRepositoryISpecExample extends ServerBaseISpec with FixedClock "version" -> JsString("1.0") ))) ), - "blocked" -> JsFalse, - "ipAllowlist" -> Json.obj("required" -> JsFalse, "allowlist" -> JsArray.empty), - "allowAutoDelete" -> JsTrue + "blocked" -> JsFalse, + "ipAllowlist" -> Json.obj("required" -> JsFalse, "allowlist" -> JsArray.empty), + "allowAutoDelete" -> JsTrue ) } @@ -225,8 +227,8 @@ class ApplicationRepositoryISpec await(notificationRepository.ensureIndexes()) } - lazy val defaultGrantLength = 547 - lazy val newGrantLength = 1000 + lazy val defaultGrantLength = GrantLength.EIGHTEEN_MONTHS.period + lazy val newGrantLength = GrantLength.ONE_MONTH.period private def generateClientId = ClientId.random @@ -347,14 +349,14 @@ class ApplicationRepositoryISpec anApplicationDataForTest( applicationId, ClientId("aaa"), - grantLength = newGrantLength + refreshTokensAvailableFor = newGrantLength ) ) ) val newRetrieved = await(applicationRepository.fetch(applicationId)).get - newRetrieved.grantLength mustBe newGrantLength + newRetrieved.refreshTokensAvailableFor mustBe newGrantLength } "set the rateLimitTier field on an Application document where none previously existed" in { @@ -406,15 +408,14 @@ class ApplicationRepositoryISpec val applicationId = ApplicationId.random await(applicationRepository.save(anApplicationDataForTest(applicationId))) - val updatedGrantLength = newGrantLength val updatedApplication = await( applicationRepository.updateApplicationGrantLength( applicationId, - updatedGrantLength + newGrantLength ) ) - updatedApplication.grantLength mustBe updatedGrantLength + updatedApplication.refreshTokensAvailableFor mustBe newGrantLength } } @@ -470,7 +471,7 @@ class ApplicationRepositoryISpec applicationId, clientId, productionState("requestorEmail@example.com"), - grantLength = newGrantLength + refreshTokensAvailableFor = newGrantLength ) .copy(lastAccess = Some(instant.minus(Duration.ofDays(20))) @@ -480,7 +481,7 @@ class ApplicationRepositoryISpec val retrieved = await(applicationRepository.findAndRecordApplicationUsage(clientId)).get - retrieved.grantLength mustBe newGrantLength + retrieved.refreshTokensAvailableFor mustBe newGrantLength } } @@ -660,8 +661,8 @@ class ApplicationRepositoryISpec } "retrieve the grant length for an application for a given client id when it has a matching client id" in { - val grantLength1 = 510 - val grantLength2 = 1000 + val grantLength1 = GrantLength.ONE_MONTH.period + val grantLength2 = GrantLength.ONE_YEAR.period val application1 = anApplicationDataForTest( ApplicationId.random, ClientId("aaa"), @@ -691,8 +692,8 @@ class ApplicationRepositoryISpec ) ) - retrieved1.map(_.grantLength) mustBe Some(grantLength1) - retrieved2.map(_.grantLength) mustBe Some(grantLength2) + retrieved1.map(_.refreshTokensAvailableFor) mustBe Some(grantLength1) + retrieved2.map(_.refreshTokensAvailableFor) mustBe Some(grantLength2) } "do not retrieve the application for a given client id when it has a matching client id but is deleted" in { @@ -3565,7 +3566,7 @@ class ApplicationRepositoryISpec prodClientId: ClientId = ClientId("aaa"), state: ApplicationState = testingState(), access: Access = Access.Standard(), - grantLength: Int = defaultGrantLength, + refreshTokensAvailableFor: Period = defaultGrantLength, users: Set[Collaborator] = Set( "user@example.com".admin() ), @@ -3583,7 +3584,7 @@ class ApplicationRepositoryISpec users, checkInformation, clientSecrets, - grantLength, + refreshTokensAvailableFor, allowAutoDelete ) } @@ -3597,7 +3598,7 @@ class ApplicationRepositoryISpec users: Set[Collaborator] = Set("user@example.com".admin()), checkInformation: Option[CheckInformation] = None, clientSecrets: List[StoredClientSecret] = List(aClientSecret(hashedSecret = "hashed-secret")), - grantLength: Int = defaultGrantLength, + refreshTokensAvailableFor: Period = defaultGrantLength, allowAutoDelete: Boolean = true ): StoredApplication = { @@ -3615,7 +3616,7 @@ class ApplicationRepositoryISpec access, instant, Some(instant), - grantLength = grantLength, + refreshTokensAvailableFor = refreshTokensAvailableFor, checkInformation = checkInformation, allowAutoDelete = allowAutoDelete ) diff --git a/it/test/uk/gov/hmrc/thirdpartyapplication/repository/TermsOfUseInvitationRepositoryISpec.scala b/it/test/uk/gov/hmrc/thirdpartyapplication/repository/TermsOfUseInvitationRepositoryISpec.scala index e9047890a..27374f983 100644 --- a/it/test/uk/gov/hmrc/thirdpartyapplication/repository/TermsOfUseInvitationRepositoryISpec.scala +++ b/it/test/uk/gov/hmrc/thirdpartyapplication/repository/TermsOfUseInvitationRepositoryISpec.scala @@ -16,28 +16,30 @@ package uk.gov.hmrc.thirdpartyapplication.repository +import java.time.temporal.ChronoUnit +import java.time.{Clock, Instant} +import scala.concurrent.ExecutionContext.Implicits.global + import org.scalatest.concurrent.Eventually import org.scalatest.prop.TableDrivenPropertyChecks import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} + import play.api.inject.bind import play.api.inject.guice.GuiceApplicationBuilder import play.api.libs.json._ -import uk.gov.hmrc.apiplatform.modules.applications.access.domain.models.Access.Standard -import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models.{Collaborator, IpAllowlist, RateLimitTier} +import uk.gov.hmrc.mongo.test.CleanMongoCollectionSupport +import uk.gov.hmrc.utils.ServerBaseISpec + import uk.gov.hmrc.apiplatform.modules.common.domain.models._ import uk.gov.hmrc.apiplatform.modules.common.utils.FixedClock -import uk.gov.hmrc.mongo.test.CleanMongoCollectionSupport +import uk.gov.hmrc.apiplatform.modules.applications.access.domain.models.Access.Standard +import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models.{Collaborator, GrantLength, IpAllowlist, RateLimitTier} import uk.gov.hmrc.thirdpartyapplication.ApplicationStateUtil import uk.gov.hmrc.thirdpartyapplication.config.SchedulerModule import uk.gov.hmrc.thirdpartyapplication.models.TermsOfUseInvitationState._ import uk.gov.hmrc.thirdpartyapplication.models._ import uk.gov.hmrc.thirdpartyapplication.models.db._ import uk.gov.hmrc.thirdpartyapplication.util.{JavaDateTimeTestUtils, MetricsHelper} -import uk.gov.hmrc.utils.ServerBaseISpec - -import java.time.temporal.ChronoUnit -import java.time.{Clock, Instant} -import scala.concurrent.ExecutionContext.Implicits.global object TermsOfUseInvitationRepositoryISpecExample extends FixedClock { val appId = ApplicationId.random @@ -475,7 +477,7 @@ class TermsOfUseInvitationRepositoryISpec Standard(), instant, Some(instant), - 547, + GrantLength.EIGHTEEN_MONTHS.period, Some(RateLimitTier.BRONZE), Environment.PRODUCTION.toString(), None, diff --git a/it/test/uk/gov/hmrc/thirdpartyapplication/repository/mongo/ApplicationRepositorySerialisationISpec.scala b/it/test/uk/gov/hmrc/thirdpartyapplication/repository/mongo/ApplicationRepositorySerialisationISpec.scala index b4663652f..d55590ba7 100644 --- a/it/test/uk/gov/hmrc/thirdpartyapplication/repository/mongo/ApplicationRepositorySerialisationISpec.scala +++ b/it/test/uk/gov/hmrc/thirdpartyapplication/repository/mongo/ApplicationRepositorySerialisationISpec.scala @@ -32,7 +32,7 @@ import uk.gov.hmrc.utils.ServerBaseISpec import uk.gov.hmrc.apiplatform.modules.common.domain.models.{ApplicationId, ClientId} 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.{ClientSecret, RedirectUri} +import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models.{ClientSecret, GrantLength, RedirectUri} import uk.gov.hmrc.thirdpartyapplication.ApplicationStateUtil import uk.gov.hmrc.thirdpartyapplication.config.SchedulerModule import uk.gov.hmrc.thirdpartyapplication.models.db._ @@ -54,8 +54,9 @@ class ApplicationRepositorySerialisationISpec protected override def appBuilder: GuiceApplicationBuilder = { GuiceApplicationBuilder() .configure( - "metrics.jvm" -> false, - "mongodb.uri" -> s"mongodb://localhost:27017/test-${this.getClass.getSimpleName}" + "grantLengthInDays" -> 1, + "metrics.jvm" -> false, + "mongodb.uri" -> s"mongodb://localhost:27017/test-${this.getClass.getSimpleName}" ) .overrides(bind[Clock].toInstance(clock)) .disable(classOf[SchedulerModule]) @@ -105,7 +106,7 @@ class ApplicationRepositorySerialisationISpec Access.Standard(), instant, Some(instant), - grantLength = grantLength, + refreshTokensAvailableFor = GrantLength.ONE_YEAR.period, checkInformation = None ) @@ -117,7 +118,7 @@ class ApplicationRepositorySerialisationISpec "repository" should { "create application with no allowAutoDelete but read it back as true" in new Setup { - val rawJson: JsObject = applicationToMongoJson(applicationData, None) + val rawJson: JsObject = applicationToMongoJson(applicationData, allowAutoDelete = None) saveApplicationAsMongoJson(rawJson) val result = await(applicationRepository.fetch(applicationId)) @@ -125,6 +126,7 @@ class ApplicationRepositorySerialisationISpec case Some(application) => { application.id mustBe applicationId application.allowAutoDelete mustBe true + application.refreshTokensAvailableFor mustBe GrantLength.ONE_YEAR.period } case None => fail() } @@ -135,12 +137,13 @@ class ApplicationRepositorySerialisationISpec appSearchResult.applications.size mustBe 1 appSearchResult.applications.head.id mustBe applicationId appSearchResult.applications.head.allowAutoDelete mustBe true + appSearchResult.applications.head.refreshTokensAvailableFor mustBe GrantLength.ONE_YEAR.period } } "create application with allowAutoDelete set to false and read it back correctly" in new Setup { - val rawJson: JsObject = applicationToMongoJson(applicationData, Some(false)) + val rawJson: JsObject = applicationToMongoJson(applicationData, allowAutoDelete = Some(false)) saveApplicationAsMongoJson(rawJson) val result = await(applicationRepository.fetch(applicationId)) @@ -154,7 +157,7 @@ class ApplicationRepositorySerialisationISpec } "create application with allowAutoDelete set to true and read it back correctly" in new Setup { - val rawJson: JsObject = applicationToMongoJson(applicationData, Some(true)) + val rawJson: JsObject = applicationToMongoJson(applicationData, allowAutoDelete = Some(true)) saveApplicationAsMongoJson(rawJson) val result = await(applicationRepository.fetch(applicationId)) @@ -170,7 +173,7 @@ class ApplicationRepositorySerialisationISpec "create application with invalid redirect UR in db and test we can read it back " in new Setup { val invalidUri = new RedirectUri("bobbins") // Using new to avoid validation of the apply method val data = applicationData.copy(access = Access.Standard().copy(redirectUris = List(invalidUri))) - val rawJson: JsObject = applicationToMongoJson(data, Some(true)) + val rawJson: JsObject = applicationToMongoJson(data, allowAutoDelete = Some(true)) saveApplicationAsMongoJson(rawJson) val result = await(applicationRepository.fetch(applicationId)) @@ -187,4 +190,95 @@ class ApplicationRepositorySerialisationISpec } } + "create application with no grantLength or refreshTokensAvailableFor. refreshTokensAvailableFor is read back as default value " in new Setup { + val rawJson: JsObject = applicationToMongoJson(applicationData, None, None, false) + saveApplicationAsMongoJson(rawJson) + val result = await(applicationRepository.fetch(applicationId)) + + result match { + case Some(application) => { + application.id mustBe applicationId + application.allowAutoDelete mustBe true + application.refreshTokensAvailableFor mustBe GrantLength.EIGHTEEN_MONTHS.period + } + case None => fail() + } + + val applicationSearch = new ApplicationSearch(filters = List(AutoDeleteAllowed)) + val appSearchResult = await(applicationRepository.searchApplications("testing")(applicationSearch)) + + appSearchResult.applications.size mustBe 1 + appSearchResult.applications.head.id mustBe applicationId + appSearchResult.applications.head.allowAutoDelete mustBe true + appSearchResult.applications.head.refreshTokensAvailableFor mustBe GrantLength.EIGHTEEN_MONTHS.period + } + + "create application with grantLength 1 day but no refreshTokensAvailableFor. refreshTokensAvailableFor is read back as 1 day " in new Setup { + val rawJson: JsObject = applicationToMongoJson(applicationData, None, Some(1), false) + saveApplicationAsMongoJson(rawJson) + val result = await(applicationRepository.fetch(applicationId)) + + result match { + case Some(application) => { + application.id mustBe applicationId + application.allowAutoDelete mustBe true + application.refreshTokensAvailableFor mustBe GrantLength.ONE_DAY.period + } + case None => fail() + } + + val applicationSearch = new ApplicationSearch(filters = List(AutoDeleteAllowed)) + val appSearchResult = await(applicationRepository.searchApplications("testing")(applicationSearch)) + + appSearchResult.applications.size mustBe 1 + appSearchResult.applications.head.id mustBe applicationId + appSearchResult.applications.head.allowAutoDelete mustBe true + appSearchResult.applications.head.refreshTokensAvailableFor mustBe GrantLength.ONE_DAY.period + } + + "create application with no grantLength but refreshTokensAvailableFor 1 month. refreshTokensAvailableFor is read back as 1 month " in new Setup { + val rawJson: JsObject = applicationToMongoJson(applicationData.copy(refreshTokensAvailableFor = GrantLength.ONE_MONTH.period), None, None, true) + saveApplicationAsMongoJson(rawJson) + val result = await(applicationRepository.fetch(applicationId)) + + result match { + case Some(application) => { + application.id mustBe applicationId + application.allowAutoDelete mustBe true + application.refreshTokensAvailableFor mustBe GrantLength.ONE_MONTH.period + } + case None => fail() + } + + val applicationSearch = new ApplicationSearch(filters = List(AutoDeleteAllowed)) + val appSearchResult = await(applicationRepository.searchApplications("testing")(applicationSearch)) + + appSearchResult.applications.size mustBe 1 + appSearchResult.applications.head.id mustBe applicationId + appSearchResult.applications.head.allowAutoDelete mustBe true + appSearchResult.applications.head.refreshTokensAvailableFor mustBe GrantLength.ONE_MONTH.period + } + + "create application with grantLength 1 day and refreshTokensAvailableFor 1 month. refreshTokensAvailableFor is read back as 1 month " in new Setup { + val rawJson: JsObject = applicationToMongoJson(applicationData.copy(refreshTokensAvailableFor = GrantLength.ONE_MONTH.period), None, Some(1), true) + saveApplicationAsMongoJson(rawJson) + val result = await(applicationRepository.fetch(applicationId)) + + result match { + case Some(application) => { + application.id mustBe applicationId + application.allowAutoDelete mustBe true + application.refreshTokensAvailableFor mustBe GrantLength.ONE_MONTH.period + } + case None => fail() + } + + val applicationSearch = new ApplicationSearch(filters = List(AutoDeleteAllowed)) + val appSearchResult = await(applicationRepository.searchApplications("testing")(applicationSearch)) + + appSearchResult.applications.size mustBe 1 + appSearchResult.applications.head.id mustBe applicationId + appSearchResult.applications.head.allowAutoDelete mustBe true + appSearchResult.applications.head.refreshTokensAvailableFor mustBe GrantLength.ONE_MONTH.period + } } diff --git a/it/test/uk/gov/hmrc/thirdpartyapplication/repository/mongo/TestRawApplicationDocuments.scala b/it/test/uk/gov/hmrc/thirdpartyapplication/repository/mongo/TestRawApplicationDocuments.scala index 4490b5559..d864137bd 100644 --- a/it/test/uk/gov/hmrc/thirdpartyapplication/repository/mongo/TestRawApplicationDocuments.scala +++ b/it/test/uk/gov/hmrc/thirdpartyapplication/repository/mongo/TestRawApplicationDocuments.scala @@ -19,7 +19,7 @@ package uk.gov.hmrc.thirdpartyapplication.repository.mongo import java.time.Instant import java.time.temporal.ChronoUnit -import play.api.libs.json.{JsBoolean, JsObject, Json} +import play.api.libs.json.{JsBoolean, JsNumber, JsObject, Json} import uk.gov.hmrc.thirdpartyapplication.models.db.StoredApplication @@ -29,8 +29,37 @@ trait TestRawApplicationDocuments { import uk.gov.hmrc.thirdpartyapplication.repository.ApplicationRepository.MongoFormats._ - def applicationToMongoJson(application: StoredApplication, allowAutoDelete: Option[Boolean] = None): JsObject = { - val applicationJson = Json.obj( + def applicationToMongoJson( + application: StoredApplication, + allowAutoDelete: Option[Boolean] = None, + grantLength: Option[Int] = Some(547), + refreshTokensAvailableFor: Boolean = true + ): JsObject = { + + def addAttributes(json: JsObject): JsObject = { + (allowAutoDelete, grantLength, refreshTokensAvailableFor) match { + case (Some(aad: Boolean), Some(gl: Int), true) => + json ++ Json.obj("allowAutoDelete" -> JsBoolean(aad), "grantLength" -> JsNumber(gl), "refreshTokensAvailableFor" -> Json.toJson(application.refreshTokensAvailableFor)) + case (Some(aad: Boolean), Some(gl: Int), false) => + json ++ Json.obj("allowAutoDelete" -> JsBoolean(aad), "grantLength" -> JsNumber(gl)) + case (Some(aad: Boolean), None, true) => + json ++ Json.obj("allowAutoDelete" -> JsBoolean(aad), "refreshTokensAvailableFor" -> Json.toJson(application.refreshTokensAvailableFor)) + case (None, Some(gl: Int), true) => + json ++ Json.obj( + "grantLength" -> JsNumber(gl), + "refreshTokensAvailableFor" -> Json.toJson(application.refreshTokensAvailableFor) + ) + case (None, None, true) => + json ++ Json.obj("refreshTokensAvailableFor" -> Json.toJson(application.refreshTokensAvailableFor)) + case (Some(aad: Boolean), None, false) => + json ++ Json.obj("allowAutoDelete" -> JsBoolean(aad)) + case (None, Some(gl: Int), false) => + json ++ Json.obj("grantLength" -> JsNumber(gl)) + case (None, None, false) => json + } + } + + val applicationJson: JsObject = Json.obj( "id" -> application.id, "name" -> application.name, "normalisedName" -> application.normalisedName, @@ -42,17 +71,12 @@ trait TestRawApplicationDocuments { "access" -> application.access, "createdOn" -> dateToJsonObj(application.createdOn), "lastAccess" -> dateToJsonObj(application.createdOn), - "grantLength" -> 547, "rateLimitTier" -> "BRONZE", "environment" -> "PRODUCTION", "blocked" -> false, "ipAllowlist" -> application.ipAllowlist ) - allowAutoDelete match { - case Some(value: Boolean) => - applicationJson + ("allowAutoDelete" -> JsBoolean(value)) - case None => applicationJson - } + addAttributes(applicationJson) } } diff --git a/shared-test/uk/gov/hmrc/thirdpartyapplication/util/ApplicationTestData.scala b/shared-test/uk/gov/hmrc/thirdpartyapplication/util/ApplicationTestData.scala index 38abdbf90..0ce35c434 100644 --- a/shared-test/uk/gov/hmrc/thirdpartyapplication/util/ApplicationTestData.scala +++ b/shared-test/uk/gov/hmrc/thirdpartyapplication/util/ApplicationTestData.scala @@ -16,6 +16,8 @@ package uk.gov.hmrc.thirdpartyapplication.util +import java.time.Period + import com.github.t3hnar.bcrypt._ import uk.gov.hmrc.apiplatform.modules.common.domain.models.LaxEmailAddress.StringSyntax @@ -36,7 +38,7 @@ trait ApplicationTestData extends ApplicationStateUtil with CollaboratorTestData val requestedByName = "john smith" val requestedByEmail = "john.smith@example.com".toLaxEmail - val grantLength = 547 + val grantLength = GrantLength.EIGHTEEN_MONTHS.period def anApplicationData( applicationId: ApplicationId, @@ -46,7 +48,7 @@ trait ApplicationTestData extends ApplicationStateUtil with CollaboratorTestData rateLimitTier: Option[RateLimitTier] = Some(RateLimitTier.BRONZE), environment: Environment = Environment.PRODUCTION, ipAllowlist: IpAllowlist = IpAllowlist(), - grantLength: Int = grantLength + grantLength: Period = grantLength ) = { StoredApplication( applicationId, diff --git a/test/uk/gov/hmrc/thirdpartyapplication/mocks/repository/ApplicationRepositoryMockModule.scala b/test/uk/gov/hmrc/thirdpartyapplication/mocks/repository/ApplicationRepositoryMockModule.scala index 244622d9d..73cfde63c 100644 --- a/test/uk/gov/hmrc/thirdpartyapplication/mocks/repository/ApplicationRepositoryMockModule.scala +++ b/test/uk/gov/hmrc/thirdpartyapplication/mocks/repository/ApplicationRepositoryMockModule.scala @@ -16,7 +16,7 @@ package uk.gov.hmrc.thirdpartyapplication.mocks.repository -import java.time.Instant +import java.time.{Instant, Period} import scala.concurrent.Future import scala.concurrent.Future.{failed, successful} @@ -275,10 +275,10 @@ trait ApplicationRepositoryMockModule extends MockitoSugar with ArgumentMatchers def thenReturn() = when(aMock.updateApplicationGrantLength(*[ApplicationId], *)).thenReturn(successful(mock[StoredApplication])) - def verifyCalledWith(applicationId: ApplicationId, newGrantLength: Int) = + def verifyCalledWith(applicationId: ApplicationId, newGrantLength: Period) = ApplicationRepoMock.verify.updateApplicationGrantLength(eqTo(applicationId), eqTo(newGrantLength)) - def thenReturnWhen(applicationId: ApplicationId, newGrantLength: Int)(updatedApplicationData: StoredApplication) = + def thenReturnWhen(applicationId: ApplicationId, newGrantLength: Period)(updatedApplicationData: StoredApplication) = when(aMock.updateApplicationGrantLength(eqTo(applicationId), eqTo(newGrantLength))).thenReturn(successful(updatedApplicationData)) } diff --git a/test/uk/gov/hmrc/thirdpartyapplication/models/db/ApplicationDataSpec.scala b/test/uk/gov/hmrc/thirdpartyapplication/models/db/ApplicationDataSpec.scala index 4adbdd55b..68e68f1c0 100644 --- a/test/uk/gov/hmrc/thirdpartyapplication/models/db/ApplicationDataSpec.scala +++ b/test/uk/gov/hmrc/thirdpartyapplication/models/db/ApplicationDataSpec.scala @@ -20,6 +20,7 @@ import uk.gov.hmrc.apiplatform.modules.common.domain.models._ import uk.gov.hmrc.apiplatform.modules.common.utils.HmrcSpec import uk.gov.hmrc.apiplatform.modules.apis.domain.models.ApiIdentifierSyntax import uk.gov.hmrc.apiplatform.modules.applications.access.domain.models.Access +import uk.gov.hmrc.apiplatform.modules.applications.core.domain.models.GrantLength import uk.gov.hmrc.apiplatform.modules.applications.core.interface.models.{ CreateApplicationRequest, CreateApplicationRequestV1, @@ -32,6 +33,7 @@ class ApplicationDataSpec extends HmrcSpec with UpliftRequestSamples with Collab import ApiIdentifierSyntax._ "StoredApplication" should { + val refreshTokensAvailableFor = GrantLength.EIGHTEEN_MONTHS.period "for version 1 requests" should { "do not set the check information when app is created without subs" in { val token = StoredToken(ClientId.random, "st") @@ -78,8 +80,7 @@ class ApplicationDataSpec extends HmrcSpec with UpliftRequestSamples with Collab subscriptions = Some(Set("context".asIdentifier)) ) - val grantLengthInDays = 547 - StoredApplication.create(request, "bob", token).grantLength shouldBe grantLengthInDays + StoredApplication.create(request, "bob", token).refreshTokensAvailableFor shouldBe refreshTokensAvailableFor } } @@ -103,7 +104,7 @@ class ApplicationDataSpec extends HmrcSpec with UpliftRequestSamples with Collab } "ensure correct grant length when app is created" in { - StoredApplication.create(request, "bob", token).grantLength shouldBe 547 + StoredApplication.create(request, "bob", token).refreshTokensAvailableFor shouldBe refreshTokensAvailableFor } } } diff --git a/test/uk/gov/hmrc/thirdpartyapplication/services/commands/grantlength/ChangeGrantLengthCommandHandlerSpec.scala b/test/uk/gov/hmrc/thirdpartyapplication/services/commands/grantlength/ChangeGrantLengthCommandHandlerSpec.scala index 46ef385e9..5130ba87c 100644 --- a/test/uk/gov/hmrc/thirdpartyapplication/services/commands/grantlength/ChangeGrantLengthCommandHandlerSpec.scala +++ b/test/uk/gov/hmrc/thirdpartyapplication/services/commands/grantlength/ChangeGrantLengthCommandHandlerSpec.scala @@ -31,7 +31,7 @@ import uk.gov.hmrc.thirdpartyapplication.services.commands.{CommandHandler, Comm class ChangeGrantLengthCommandHandlerSpec extends CommandHandlerBaseSpec { val originalGrantLength = GrantLength.SIX_MONTHS - val app = principalApp.copy(grantLength = originalGrantLength.period.getDays) + val app = principalApp.copy(refreshTokensAvailableFor = originalGrantLength.period) trait Setup extends ApplicationRepositoryMockModule { @@ -39,7 +39,7 @@ class ChangeGrantLengthCommandHandlerSpec extends CommandHandlerBaseSpec { val gatekeeperUser = "gkuser" val replaceWithGrantLength = GrantLength.ONE_YEAR - val newApp = app.copy(grantLength = replaceWithGrantLength.period.getDays) + val newApp = app.copy(refreshTokensAvailableFor = replaceWithGrantLength.period) val timestamp = FixedClock.instant val update = ApplicationCommands.ChangeGrantLength(gatekeeperUser, instant, replaceWithGrantLength) @@ -67,14 +67,14 @@ class ChangeGrantLengthCommandHandlerSpec extends CommandHandlerBaseSpec { "process" should { "create correct events for a valid request with app" in new Setup { - ApplicationRepoMock.UpdateGrantLength.thenReturnWhen(app.id, replaceWithGrantLength.period.getDays)(newApp) + ApplicationRepoMock.UpdateGrantLength.thenReturnWhen(app.id, replaceWithGrantLength.period)(newApp) checkSuccessResult(Actors.GatekeeperUser(gatekeeperUser)) { underTest.process(app, update) } } - "return an error if the application already has the specified grant length" in new Setup { + "return an error if the application already has refreshTokenAvailableFor 180 days" in new Setup { val updateWithSameGrantLength = update.copy(grantLength = GrantLength.SIX_MONTHS) checkFailsWith("Grant length is already 180 days") { @@ -82,5 +82,13 @@ class ChangeGrantLengthCommandHandlerSpec extends CommandHandlerBaseSpec { } } + "return an error if the application already has refreshTokenAvailableFor 0 days (i.e. 4 hours and no refresh tokens)" in new Setup { + val updateWithSameGrantLength = update.copy(grantLength = GrantLength.FOUR_HOURS) + + checkFailsWith("Grant length is already 4 hours and no refresh tokens") { + underTest.process(app.copy(refreshTokensAvailableFor = GrantLength.FOUR_HOURS.period), updateWithSameGrantLength) + } + } + } }