From c9cbd74ea94f33336c88c0c569f79f8707841778 Mon Sep 17 00:00:00 2001 From: Kurt Logan Date: Sun, 18 Nov 2018 11:58:02 +0000 Subject: [PATCH 01/23] fixed issue with displaying and testing textareas --- .../views/components/input_textarea.scala.html | 15 ++++++++------- src/main/g8/test/views/ViewSpecBase.scala | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/main/g8/app/views/components/input_textarea.scala.html b/src/main/g8/app/views/components/input_textarea.scala.html index 0f502775..15a4c5bf 100644 --- a/src/main/g8/app/views/components/input_textarea.scala.html +++ b/src/main/g8/app/views/components/input_textarea.scala.html @@ -19,13 +19,14 @@
+ class="form-control form-control--full-width @inputClass" + id="@{field.id}" + name="@{field.id}" + @if(field.hasErrors) { aria-describedby="error-message-@{field.id}-input" } + rows="5"> + + @{field.value} +
diff --git a/src/main/g8/test/views/ViewSpecBase.scala b/src/main/g8/test/views/ViewSpecBase.scala index 08ab6029..df041088 100644 --- a/src/main/g8/test/views/ViewSpecBase.scala +++ b/src/main/g8/test/views/ViewSpecBase.scala @@ -53,7 +53,7 @@ trait ViewSpecBase extends SpecBase { val labels = doc.getElementsByAttributeValue("for", forElement) assert(labels.size == 1, s"\n\nLabel for \$forElement was not rendered on the page.") val label = labels.first - assert(label.text() == expectedText, s"\n\nLabel for \$forElement was not \$expectedText") + assert(label.text().contains(expectedText), s"\n\nLabel for \$forElement was not \$expectedText") if (expectedHintText.isDefined) { assert(label.getElementsByClass("form-hint").first.text == expectedHintText.get, From 662208b958cafb8d9a176c848e9fcb03fff7967b Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Sat, 10 Nov 2018 15:11:06 +0000 Subject: [PATCH 02/23] Migration to Play 2.6 --- .../g8/app/config/FrontendAppConfig.scala | 43 ++-- src/main/g8/app/config/Module.scala | 21 +- src/main/g8/app/config/Service.scala | 32 +++ .../app/connectors/DataCacheConnector.scala | 33 --- .../CheckYourAnswersController.scala | 28 ++- .../g8/app/controllers/IndexController.scala | 18 +- .../LanguageSwitchController.scala | 27 ++- .../SessionExpiredController.scala | 20 +- .../controllers/UnauthorisedController.scala | 18 +- .../actions/DataRequiredAction.scala | 20 +- .../actions/DataRetrievalAction.scala | 23 +- .../actions/IdentifierAction.scala | 55 ++--- src/main/g8/app/filters/Filters.scala | 10 - .../g8/app/filters/FiltersWithWhitelist.scala | 11 - src/main/g8/app/filters/SessionIdFilter.scala | 53 ++--- src/main/g8/app/handlers/ErrorHandler.scala | 10 +- src/main/g8/app/models/UserAnswers.scala | 11 +- src/main/g8/app/models/UserData.scala | 41 ++++ .../app/repositories/SessionRepository.scala | 105 +++++----- ...a.html => CheckYourAnswersView.scala.html} | 14 +- ...te.scala.html => ErrorTemplate.scala.html} | 7 +- ...per.scala.html => GovukWrapper.scala.html} | 44 ++-- src/main/g8/app/views/IndexView.scala.html | 14 ++ ...ate.scala.html => MainTemplate.scala.html} | 16 +- .../app/views/SessionExpiredView.scala.html | 14 ++ .../g8/app/views/UnauthorisedView.scala.html | 12 ++ .../components/language_selection.scala.html | 17 ++ src/main/g8/app/views/index.scala.html | 13 -- .../g8/app/views/session_expired.scala.html | 13 -- src/main/g8/app/views/unauthorised.scala.html | 12 -- src/main/g8/build.sbt | 23 +- src/main/g8/conf/application.conf | 68 +++--- src/main/g8/migrate.sh | 8 + src/main/g8/project/AppDependencies.scala | 36 ++-- src/main/g8/project/plugins.sbt | 6 +- src/main/g8/test/base/SpecBase.scala | 18 +- .../connectors/FakeDataCacheConnector.scala | 17 -- .../connectors/MongoCacheConnectorSpec.scala | 198 ------------------ .../CheckYourAnswersControllerSpec.scala | 41 ++-- .../test/controllers/ControllerSpecBase.scala | 16 -- .../controllers/IndexControllerSpec.scala | 27 ++- .../SessionExpiredControllerSpec.scala | 27 ++- .../UnauthorisedControllerSpec.scala | 27 ++- .../controllers/actions/AuthActionSpec.scala | 86 ++++++-- .../actions/DataRetrievalActionSpec.scala | 30 +-- .../actions/FakeDataRetrievalAction.scala | 22 +- .../actions/FakeIdentifierAction.scala | 17 +- .../actions/SessionActionSpec.scala | 49 +++-- .../g8/test/filters/SessionIdFilterSpec.scala | 34 +-- src/main/g8/test/generators/Generators.scala | 3 +- ...enerator.scala => UserDataGenerator.scala} | 14 +- src/main/g8/test/models/EnumerableSpec.scala | 3 +- .../pages/behaviours/PageBehaviours.scala | 37 ++-- src/main/g8/test/resources/logback.xml | 2 +- .../g8/test/resources/test.application.conf | 12 ++ src/main/g8/test/views/IndexViewSpec.scala | 12 +- .../test/views/SessionExpiredViewSpec.scala | 12 +- .../g8/test/views/UnauthorisedViewSpec.scala | 12 +- .../views/behaviours/IntViewBehaviours.scala | 17 +- .../behaviours/QuestionViewBehaviours.scala | 21 +- .../behaviours/StringViewBehaviours.scala | 13 +- .../views/behaviours/ViewBehaviours.scala | 30 ++- .../behaviours/YesNoViewBehaviours.scala | 23 +- .../controllers/$className$Controller.scala | 41 ++-- .../app/forms/$className$FormProvider.scala | 3 +- ....scala.html => $className$View.scala.html} | 18 +- .../$className$ControllerSpec.scala | 122 +++++++---- .../views/$className$ViewSpec.scala | 21 +- .../intPage/migrations/$className__snake$.sh | 15 +- .../controllers/$className$Controller.scala | 51 ++--- ....scala.html => $className$View.scala.html} | 20 +- .../$className$ControllerSpec.scala | 121 +++++++---- .../pages/$className$PageSpec.scala | 2 +- .../views/$className$ViewSpec.scala | 40 ++-- .../migrations/$className__snake$.sh | 9 +- .../controllers/$className$Controller.scala | 29 +-- .../page/app/views/$className$View.scala.html | 14 ++ .../app/views/$className__decap$.scala.html | 13 -- .../$className$ControllerSpec.scala | 30 +-- .../views/$className$ViewSpec.scala | 15 +- .../page/migrations/$className__snake$.sh | 1 + .../controllers/$className$Controller.scala | 42 ++-- ....scala.html => $className$View.scala.html} | 19 +- .../$className$ControllerSpec.scala | 124 +++++++---- .../views/$className$ViewSpec.scala | 21 +- .../migrations/$className__snake$.sh | 7 +- .../controllers/$className$Controller.scala | 41 ++-- ....scala.html => $className$View.scala.html} | 17 +- .../$className$ControllerSpec.scala | 120 +++++++---- .../views/$className$ViewSpec.scala | 21 +- .../migrations/$className__snake$.sh | 5 +- .../controllers/$className$Controller.scala | 42 ++-- ....scala.html => $className$View.scala.html} | 17 +- .../$className$ControllerSpec.scala | 121 +++++++---- .../views/$className$ViewSpec.scala | 22 +- .../migrations/$className__snake$.sh | 5 +- 96 files changed, 1606 insertions(+), 1299 deletions(-) create mode 100644 src/main/g8/app/config/Service.scala delete mode 100644 src/main/g8/app/connectors/DataCacheConnector.scala delete mode 100644 src/main/g8/app/filters/Filters.scala delete mode 100644 src/main/g8/app/filters/FiltersWithWhitelist.scala create mode 100644 src/main/g8/app/models/UserData.scala rename src/main/g8/app/views/{check_your_answers.scala.html => CheckYourAnswersView.scala.html} (56%) rename src/main/g8/app/views/{error_template.scala.html => ErrorTemplate.scala.html} (56%) rename src/main/g8/app/views/{govuk_wrapper.scala.html => GovukWrapper.scala.html} (66%) create mode 100644 src/main/g8/app/views/IndexView.scala.html rename src/main/g8/app/views/{main_template.scala.html => MainTemplate.scala.html} (71%) create mode 100644 src/main/g8/app/views/SessionExpiredView.scala.html create mode 100644 src/main/g8/app/views/UnauthorisedView.scala.html create mode 100644 src/main/g8/app/views/components/language_selection.scala.html delete mode 100644 src/main/g8/app/views/index.scala.html delete mode 100644 src/main/g8/app/views/session_expired.scala.html delete mode 100644 src/main/g8/app/views/unauthorised.scala.html delete mode 100644 src/main/g8/test/connectors/FakeDataCacheConnector.scala delete mode 100644 src/main/g8/test/connectors/MongoCacheConnectorSpec.scala delete mode 100644 src/main/g8/test/controllers/ControllerSpecBase.scala rename src/main/g8/test/generators/{CacheMapGenerator.scala => UserDataGenerator.scala} (63%) create mode 100644 src/main/g8/test/resources/test.application.conf rename src/main/scaffolds/intPage/app/views/{$className__decap$.scala.html => $className$View.scala.html} (53%) rename src/main/scaffolds/optionsPage/app/views/{$className__decap$.scala.html => $className$View.scala.html} (57%) create mode 100644 src/main/scaffolds/page/app/views/$className$View.scala.html delete mode 100644 src/main/scaffolds/page/app/views/$className__decap$.scala.html rename src/main/scaffolds/questionPage/app/views/{$className__decap$.scala.html => $className$View.scala.html} (58%) rename src/main/scaffolds/stringPage/app/views/{$className__decap$.scala.html => $className$View.scala.html} (57%) rename src/main/scaffolds/yesNoPage/app/views/{$className__decap$.scala.html => $className$View.scala.html} (58%) diff --git a/src/main/g8/app/config/FrontendAppConfig.scala b/src/main/g8/app/config/FrontendAppConfig.scala index f95ee97f..5de6197a 100644 --- a/src/main/g8/app/config/FrontendAppConfig.scala +++ b/src/main/g8/app/config/FrontendAppConfig.scala @@ -1,35 +1,36 @@ package config import com.google.inject.{Inject, Singleton} -import play.api.{Configuration, Environment} -import play.api.i18n.Lang import controllers.routes -import uk.gov.hmrc.play.config.ServicesConfig +import play.api.Configuration +import play.api.i18n.Lang +import play.api.mvc.Call @Singleton -class FrontendAppConfig @Inject() (override val runModeConfiguration: Configuration, environment: Environment) extends ServicesConfig { +class FrontendAppConfig @Inject() (configuration: Configuration) { - override protected def mode = environment.mode + private val contactHost = configuration.get[String]("contact-frontend.host") + private val contactFormServiceIdentifier = "play26frontend" - private def loadConfig(key: String) = runModeConfiguration.getString(key).getOrElse(throw new Exception(s"Missing configuration key: \$key")) + val analyticsToken: String = configuration.get[String](s"google-analytics.token") + val analyticsHost: String = configuration.get[String](s"google-analytics.host") + val reportAProblemPartialUrl = s"\$contactHost/contact/problem_reports_ajax?service=\$contactFormServiceIdentifier" + val reportAProblemNonJSUrl = s"\$contactHost/contact/problem_reports_nonjs?service=\$contactFormServiceIdentifier" + val betaFeedbackUrl = s"\$contactHost/contact/beta-feedback" + val betaFeedbackUnauthenticatedUrl = s"\$contactHost/contact/beta-feedback-unauthenticated" - private lazy val contactHost = runModeConfiguration.getString("contact-frontend.host").getOrElse("") - private val contactFormServiceIdentifier = "$name;format="lower,word"$" + lazy val authUrl: String = configuration.get[Service]("auth").baseUrl + lazy val loginUrl: String = configuration.get[String]("urls.login") + lazy val loginContinueUrl: String = configuration.get[String]("urls.loginContinue") - lazy val analyticsToken = loadConfig(s"google-analytics.token") - lazy val analyticsHost = loadConfig(s"google-analytics.host") - lazy val reportAProblemPartialUrl = s"\$contactHost/contact/problem_reports_ajax?service=\$contactFormServiceIdentifier" - lazy val reportAProblemNonJSUrl = s"\$contactHost/contact/problem_reports_nonjs?service=\$contactFormServiceIdentifier" - lazy val betaFeedbackUrl = s"\$contactHost/contact/beta-feedback" - lazy val betaFeedbackUnauthenticatedUrl = s"\$contactHost/contact/beta-feedback-unauthenticated" + lazy val languageTranslationEnabled: Boolean = + configuration.get[Boolean]("microservice.services.features.welsh-translation") - lazy val authUrl = baseUrl("auth") - lazy val loginUrl = loadConfig("urls.login") - lazy val loginContinueUrl = loadConfig("urls.loginContinue") - - lazy val languageTranslationEnabled = runModeConfiguration.getBoolean("microservice.services.features.welsh-translation").getOrElse(true) def languageMap: Map[String, Lang] = Map( "english" -> Lang("en"), - "cymraeg" -> Lang("cy")) - def routeToSwitchLanguage = (lang: String) => routes.LanguageSwitchController.switchToLanguage(lang) + "cymraeg" -> Lang("cy") + ) + + def routeToSwitchLanguage: String => Call = + (lang: String) => routes.LanguageSwitchController.switchToLanguage(lang) } diff --git a/src/main/g8/app/config/Module.scala b/src/main/g8/app/config/Module.scala index 1c1ba8ad..dbb0ee9e 100644 --- a/src/main/g8/app/config/Module.scala +++ b/src/main/g8/app/config/Module.scala @@ -1,36 +1,19 @@ -/* - * Copyright 2018 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 config import com.google.inject.AbstractModule -import connectors._ import controllers.actions._ +import repositories.{DefaultSessionRepository, SessionRepository} class Module extends AbstractModule { override def configure(): Unit = { - // Bind the actions for DI bind(classOf[DataRetrievalAction]).to(classOf[DataRetrievalActionImpl]).asEagerSingleton() bind(classOf[DataRequiredAction]).to(classOf[DataRequiredActionImpl]).asEagerSingleton() // For session based storage instead of cred based, change to SessionIdentifierAction bind(classOf[IdentifierAction]).to(classOf[AuthenticatedIdentifierAction]).asEagerSingleton() - bind(classOf[DataCacheConnector]).to(classOf[MongoCacheConnector]).asEagerSingleton() + bind(classOf[SessionRepository]).to(classOf[DefaultSessionRepository]).asEagerSingleton() } } diff --git a/src/main/g8/app/config/Service.scala b/src/main/g8/app/config/Service.scala new file mode 100644 index 00000000..4b5638ec --- /dev/null +++ b/src/main/g8/app/config/Service.scala @@ -0,0 +1,32 @@ +package config + +import play.api.{ConfigLoader, Configuration} + +import scala.language.implicitConversions + +final case class Service(host: String, port: String, protocol: String) { + + def baseUrl: String = + s"\$protocol://\$host:\$port" + + override def toString: String = + baseUrl +} + +object Service { + + implicit lazy val configLoader: ConfigLoader[Service] = ConfigLoader { + config => + prefix => + + val service = Configuration(config).get[Configuration](prefix) + val host = service.get[String]("host") + val port = service.get[String]("port") + val protocol = service.get[String]("protocol") + + Service(host, port, protocol) + } + + implicit def convertToString(service: Service): String = + service.baseUrl +} diff --git a/src/main/g8/app/connectors/DataCacheConnector.scala b/src/main/g8/app/connectors/DataCacheConnector.scala deleted file mode 100644 index 9e8471c9..00000000 --- a/src/main/g8/app/connectors/DataCacheConnector.scala +++ /dev/null @@ -1,33 +0,0 @@ -package connectors - -import com.google.inject.{ImplementedBy, Inject} -import play.api.libs.json.Format -import uk.gov.hmrc.http.cache.client.CacheMap -import repositories.SessionRepository -import scala.concurrent.ExecutionContext.Implicits.global - -import scala.concurrent.Future - -class MongoCacheConnector @Inject()(val sessionRepository: SessionRepository) extends DataCacheConnector { - - def save[A](cacheMap: CacheMap): Future[CacheMap] = { - sessionRepository().upsert(cacheMap).map{_ => cacheMap} - } - - def fetch(cacheId: String): Future[Option[CacheMap]] = - sessionRepository().get(cacheId) - - def getEntry[A](cacheId: String, key: String)(implicit fmt: Format[A]): Future[Option[A]] = { - fetch(cacheId).map { optionalCacheMap => - optionalCacheMap.flatMap { cacheMap => cacheMap.getEntry(key)} - } - } -} - -trait DataCacheConnector { - def save[A](cacheMap: CacheMap): Future[CacheMap] - - def fetch(cacheId: String): Future[Option[CacheMap]] - - def getEntry[A](cacheId: String, key: String)(implicit fmt: Format[A]): Future[Option[A]] -} diff --git a/src/main/g8/app/controllers/CheckYourAnswersController.scala b/src/main/g8/app/controllers/CheckYourAnswersController.scala index 08bbd650..c264f3fb 100644 --- a/src/main/g8/app/controllers/CheckYourAnswersController.scala +++ b/src/main/g8/app/controllers/CheckYourAnswersController.scala @@ -1,24 +1,30 @@ package controllers import com.google.inject.Inject +import controllers.actions.{DataRequiredAction, DataRetrievalAction, IdentifierAction} import play.api.i18n.{I18nSupport, MessagesApi} -import controllers.actions.{IdentifierAction, DataRequiredAction, DataRetrievalAction} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import uk.gov.hmrc.play.bootstrap.controller.FrontendBaseController import utils.CheckYourAnswersHelper import viewmodels.AnswerSection -import views.html.check_your_answers -import config.FrontendAppConfig -import uk.gov.hmrc.play.bootstrap.controller.FrontendController +import views.html.CheckYourAnswersView -class CheckYourAnswersController @Inject()(appConfig: FrontendAppConfig, - override val messagesApi: MessagesApi, - authenticate: IdentifierAction, - getData: DataRetrievalAction, - requireData: DataRequiredAction) extends FrontendController with I18nSupport { +class CheckYourAnswersController @Inject()( + override val messagesApi: MessagesApi, + identify: IdentifierAction, + getData: DataRetrievalAction, + requireData: DataRequiredAction, + val controllerComponents: MessagesControllerComponents, + view: CheckYourAnswersView + ) extends FrontendBaseController with I18nSupport { - def onPageLoad() = (authenticate andThen getData andThen requireData) { + def onPageLoad(): Action[AnyContent] = (identify andThen getData andThen requireData) { implicit request => + val checkYourAnswersHelper = new CheckYourAnswersHelper(request.userAnswers) + val sections = Seq(AnswerSection(None, Seq())) - Ok(check_your_answers(appConfig, sections)) + + Ok(view(sections)) } } diff --git a/src/main/g8/app/controllers/IndexController.scala b/src/main/g8/app/controllers/IndexController.scala index bc6ef254..a6894c22 100644 --- a/src/main/g8/app/controllers/IndexController.scala +++ b/src/main/g8/app/controllers/IndexController.scala @@ -1,17 +1,17 @@ package controllers import javax.inject.Inject +import play.api.i18n.I18nSupport +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import uk.gov.hmrc.play.bootstrap.controller.FrontendBaseController +import views.html.IndexView -import play.api.i18n.{I18nSupport, MessagesApi} -import play.api.mvc.{Action, AnyContent} -import uk.gov.hmrc.play.bootstrap.controller.FrontendController -import config.FrontendAppConfig -import views.html.index - -class IndexController @Inject()(val appConfig: FrontendAppConfig, - val messagesApi: MessagesApi) extends FrontendController with I18nSupport { +class IndexController @Inject()( + val controllerComponents: MessagesControllerComponents, + view: IndexView + ) extends FrontendBaseController with I18nSupport { def onPageLoad: Action[AnyContent] = Action { implicit request => - Ok(index(appConfig)) + Ok(view()) } } diff --git a/src/main/g8/app/controllers/LanguageSwitchController.scala b/src/main/g8/app/controllers/LanguageSwitchController.scala index 03aee471..b1fcbeae 100644 --- a/src/main/g8/app/controllers/LanguageSwitchController.scala +++ b/src/main/g8/app/controllers/LanguageSwitchController.scala @@ -1,20 +1,18 @@ package controllers import com.google.inject.Inject +import config.FrontendAppConfig import play.api.Configuration import play.api.i18n.{I18nSupport, Lang, MessagesApi} -import play.api.mvc.{Action, AnyContent, Call, Controller} -import config.FrontendAppConfig -import uk.gov.hmrc.play.language.LanguageUtils +import play.api.mvc._ +import uk.gov.hmrc.play.bootstrap.controller.FrontendBaseController -// TODO, upstream this into play-language -class LanguageSwitchController @Inject() ( - configuration: Configuration, - appConfig: FrontendAppConfig, - implicit val messagesApi: MessagesApi - ) extends Controller with I18nSupport { - - private def langToCall(lang: String): (String) => Call = appConfig.routeToSwitchLanguage +class LanguageSwitchController @Inject()( + configuration: Configuration, + appConfig: FrontendAppConfig, + override implicit val messagesApi: MessagesApi, + val controllerComponents: MessagesControllerComponents + ) extends FrontendBaseController with I18nSupport { private def fallbackURL: String = routes.IndexController.onPageLoad().url @@ -22,16 +20,17 @@ class LanguageSwitchController @Inject() ( def switchToLanguage(language: String): Action[AnyContent] = Action { implicit request => + val enabled = isWelshEnabled val lang = if (enabled) { - languageMap.getOrElse(language, LanguageUtils.getCurrentLang) + languageMap.getOrElse(language, Lang.defaultLang) } else { Lang("en") } val redirectURL = request.headers.get(REFERER).getOrElse(fallbackURL) - Redirect(redirectURL).withLang(Lang.apply(lang.code)).flashing(LanguageUtils.FlashWithSwitchIndicator) + Redirect(redirectURL).withLang(Lang.apply(lang.code)) } private def isWelshEnabled: Boolean = - configuration.getBoolean("microservice.services.features.welsh-translation").getOrElse(true) + configuration.getOptional[Boolean]("microservice.services.features.welsh-translation").getOrElse(true) } diff --git a/src/main/g8/app/controllers/SessionExpiredController.scala b/src/main/g8/app/controllers/SessionExpiredController.scala index 3e7c6ffc..caf9672b 100644 --- a/src/main/g8/app/controllers/SessionExpiredController.scala +++ b/src/main/g8/app/controllers/SessionExpiredController.scala @@ -1,19 +1,17 @@ package controllers import javax.inject.Inject +import play.api.i18n.I18nSupport +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import uk.gov.hmrc.play.bootstrap.controller.FrontendBaseController +import views.html.SessionExpiredView -import play.api.i18n.{I18nSupport, MessagesApi} -import play.api.mvc.{Action, AnyContent} -import uk.gov.hmrc.play.bootstrap.controller.FrontendController -import config.FrontendAppConfig -import views.html.session_expired - -import scala.concurrent.Future - -class SessionExpiredController @Inject()(val appConfig: FrontendAppConfig, - val messagesApi: MessagesApi) extends FrontendController with I18nSupport { +class SessionExpiredController @Inject()( + val controllerComponents: MessagesControllerComponents, + view: SessionExpiredView + ) extends FrontendBaseController with I18nSupport { def onPageLoad: Action[AnyContent] = Action { implicit request => - Ok(session_expired(appConfig)) + Ok(view()) } } diff --git a/src/main/g8/app/controllers/UnauthorisedController.scala b/src/main/g8/app/controllers/UnauthorisedController.scala index eb876baf..bea983aa 100644 --- a/src/main/g8/app/controllers/UnauthorisedController.scala +++ b/src/main/g8/app/controllers/UnauthorisedController.scala @@ -1,17 +1,17 @@ package controllers import javax.inject.Inject +import play.api.i18n.I18nSupport +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import uk.gov.hmrc.play.bootstrap.controller.FrontendBaseController +import views.html.UnauthorisedView -import play.api.i18n.{I18nSupport, MessagesApi} -import play.api.mvc.{Action, AnyContent} -import uk.gov.hmrc.play.bootstrap.controller.FrontendController -import config.FrontendAppConfig -import views.html.unauthorised - -class UnauthorisedController @Inject()(val appConfig: FrontendAppConfig, - val messagesApi: MessagesApi) extends FrontendController with I18nSupport { +class UnauthorisedController @Inject()( + val controllerComponents: MessagesControllerComponents, + view: UnauthorisedView + ) extends FrontendBaseController with I18nSupport { def onPageLoad: Action[AnyContent] = Action { implicit request => - Ok(unauthorised(appConfig)) + Ok(view()) } } diff --git a/src/main/g8/app/controllers/actions/DataRequiredAction.scala b/src/main/g8/app/controllers/actions/DataRequiredAction.scala index acb7f57c..58b7289b 100644 --- a/src/main/g8/app/controllers/actions/DataRequiredAction.scala +++ b/src/main/g8/app/controllers/actions/DataRequiredAction.scala @@ -1,25 +1,25 @@ package controllers.actions -import com.google.inject.Inject -import play.api.mvc.{ActionRefiner, Result} -import play.api.mvc.Results.Redirect +import javax.inject.Inject import controllers.routes -import models.UserAnswers import models.requests.{DataRequest, OptionalDataRequest} -import uk.gov.hmrc.http.HeaderCarrier +import play.api.mvc.Results.Redirect +import play.api.mvc.{ActionRefiner, Result} import uk.gov.hmrc.play.HeaderCarrierConverter -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} -class DataRequiredActionImpl @Inject() extends DataRequiredAction { +class DataRequiredActionImpl @Inject()(implicit val executionContext: ExecutionContext) extends DataRequiredAction { override protected def refine[A](request: OptionalDataRequest[A]): Future[Either[Result, DataRequest[A]]] = { + implicit val hc = HeaderCarrierConverter.fromHeadersAndSession(request.headers, Some(request.session)) request.userAnswers match { - case None => Future.successful(Left(Redirect(routes.SessionExpiredController.onPageLoad()))) - case Some(data) => Future.successful(Right(DataRequest(request.request, request.internalId, data))) + case None => + Future.successful(Left(Redirect(routes.SessionExpiredController.onPageLoad()))) + case Some(data) => + Future.successful(Right(DataRequest(request.request, request.internalId, data))) } } } diff --git a/src/main/g8/app/controllers/actions/DataRetrievalAction.scala b/src/main/g8/app/controllers/actions/DataRetrievalAction.scala index eff82578..fd0c3b40 100644 --- a/src/main/g8/app/controllers/actions/DataRetrievalAction.scala +++ b/src/main/g8/app/controllers/actions/DataRetrievalAction.scala @@ -1,24 +1,27 @@ package controllers.actions -import com.google.inject.Inject -import play.api.mvc.ActionTransformer -import connectors.DataCacheConnector +import javax.inject.Inject import models.UserAnswers import models.requests.{IdentifierRequest, OptionalDataRequest} -import uk.gov.hmrc.http.HeaderCarrier +import play.api.mvc.ActionTransformer +import repositories.SessionRepository import uk.gov.hmrc.play.HeaderCarrierConverter -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} -class DataRetrievalActionImpl @Inject()(val dataCacheConnector: DataCacheConnector) extends DataRetrievalAction { +class DataRetrievalActionImpl @Inject()( + val sessionRepository: SessionRepository + )(implicit val executionContext: ExecutionContext) extends DataRetrievalAction { override protected def transform[A](request: IdentifierRequest[A]): Future[OptionalDataRequest[A]] = { + implicit val hc = HeaderCarrierConverter.fromHeadersAndSession(request.headers, Some(request.session)) - dataCacheConnector.fetch(request.identifier).map { - case None => OptionalDataRequest(request.request, request.identifier, None) - case Some(data) => OptionalDataRequest(request.request, request.identifier, Some(UserAnswers(data))) + sessionRepository.get(request.identifier).map { + case None => + OptionalDataRequest(request.request, request.identifier, None) + case Some(data) => + OptionalDataRequest(request.request, request.identifier, Some(UserAnswers(data))) } } } diff --git a/src/main/g8/app/controllers/actions/IdentifierAction.scala b/src/main/g8/app/controllers/actions/IdentifierAction.scala index b13eb6d2..6d51ac13 100644 --- a/src/main/g8/app/controllers/actions/IdentifierAction.scala +++ b/src/main/g8/app/controllers/actions/IdentifierAction.scala @@ -1,23 +1,27 @@ package controllers.actions import com.google.inject.Inject -import play.api.mvc.{ActionBuilder, ActionFunction, Request, Result} -import play.api.mvc.Results._ -import uk.gov.hmrc.auth.core._ -import uk.gov.hmrc.auth.core.retrieve.Retrievals import config.FrontendAppConfig import controllers.routes import models.requests.IdentifierRequest -import uk.gov.hmrc.http.UnauthorizedException -import uk.gov.hmrc.http.HeaderCarrier +import play.api.mvc.Results._ +import play.api.mvc._ +import uk.gov.hmrc.auth.core._ +import uk.gov.hmrc.auth.core.retrieve.Retrievals +import uk.gov.hmrc.http.{HeaderCarrier, UnauthorizedException} import uk.gov.hmrc.play.HeaderCarrierConverter import scala.concurrent.{ExecutionContext, Future} -class AuthenticatedIdentifierAction @Inject()(override val authConnector: AuthConnector, config: FrontendAppConfig) - (implicit ec: ExecutionContext) extends IdentifierAction with AuthorisedFunctions { +class AuthenticatedIdentifierAction @Inject()( + override val authConnector: AuthConnector, + config: FrontendAppConfig, + val parser: BodyParsers.Default + ) + (implicit val executionContext: ExecutionContext) extends IdentifierAction with AuthorisedFunctions { - override def invokeBlock[A](request: Request[A], block: (IdentifierRequest[A]) => Future[Result]): Future[Result] = { + override def invokeBlock[A](request: Request[A], block: IdentifierRequest[A] => Future[Result]): Future[Result] = { + implicit val hc: HeaderCarrier = HeaderCarrierConverter.fromHeadersAndSession(request.headers, Some(request.session)) authorised().retrieve(Retrievals.internalId) { @@ -25,33 +29,32 @@ class AuthenticatedIdentifierAction @Inject()(override val authConnector: AuthCo internalId => block(IdentifierRequest(request, internalId)) }.getOrElse(throw new UnauthorizedException("Unable to retrieve internal Id")) } recover { - case ex: NoActiveSession => + case _: NoActiveSession => Redirect(config.loginUrl, Map("continue" -> Seq(config.loginContinueUrl))) - case ex: InsufficientEnrolments => - Redirect(routes.UnauthorisedController.onPageLoad) - case ex: InsufficientConfidenceLevel => - Redirect(routes.UnauthorisedController.onPageLoad) - case ex: UnsupportedAuthProvider => - Redirect(routes.UnauthorisedController.onPageLoad) - case ex: UnsupportedAffinityGroup => - Redirect(routes.UnauthorisedController.onPageLoad) - case ex: UnsupportedCredentialRole => - Redirect(routes.UnauthorisedController.onPageLoad) + case _ => + Redirect(routes.UnauthorisedController.onPageLoad()) + } } } -trait IdentifierAction extends ActionBuilder[IdentifierRequest] with ActionFunction[Request, IdentifierRequest] +trait IdentifierAction extends ActionBuilder[IdentifierRequest, AnyContent] with ActionFunction[Request, IdentifierRequest] -class SessionIdentifierAction @Inject()(config: FrontendAppConfig) - (implicit ec: ExecutionContext) extends IdentifierAction { +class SessionIdentifierAction @Inject()( + config: FrontendAppConfig, + val parser: BodyParsers.Default + ) + (implicit val executionContext: ExecutionContext) extends IdentifierAction { - override def invokeBlock[A](request: Request[A], block: (IdentifierRequest[A]) => Future[Result]): Future[Result] = { + override def invokeBlock[A](request: Request[A], block: IdentifierRequest[A] => Future[Result]): Future[Result] = { + implicit val hc: HeaderCarrier = HeaderCarrierConverter.fromHeadersAndSession(request.headers, Some(request.session)) hc.sessionId match { - case Some(session) => block(IdentifierRequest(request, session.value)) - case None => Future.successful(Redirect(routes.SessionExpiredController.onPageLoad())) + case Some(session) => + block(IdentifierRequest(request, session.value)) + case None => + Future.successful(Redirect(routes.SessionExpiredController.onPageLoad())) } } } diff --git a/src/main/g8/app/filters/Filters.scala b/src/main/g8/app/filters/Filters.scala deleted file mode 100644 index 4480612c..00000000 --- a/src/main/g8/app/filters/Filters.scala +++ /dev/null @@ -1,10 +0,0 @@ -package filters - -import com.google.inject.Inject -import play.api.http.DefaultHttpFilters -import uk.gov.hmrc.play.bootstrap.filters.FrontendFilters - -class Filters @Inject() ( - sessionIdFilter: SessionIdFilter, - frontendFilters: FrontendFilters - ) extends DefaultHttpFilters(frontendFilters.filters :+ sessionIdFilter: _*) diff --git a/src/main/g8/app/filters/FiltersWithWhitelist.scala b/src/main/g8/app/filters/FiltersWithWhitelist.scala deleted file mode 100644 index e2497d34..00000000 --- a/src/main/g8/app/filters/FiltersWithWhitelist.scala +++ /dev/null @@ -1,11 +0,0 @@ -package filters - -import javax.inject.Inject -import play.api.http.DefaultHttpFilters -import uk.gov.hmrc.play.bootstrap.filters.FrontendFilters - -class FiltersWithWhitelist @Inject()( - frontendFilters: FrontendFilters, - whitelistFilter: WhitelistFilter, - sessionIdFilter: SessionIdFilter - ) extends DefaultHttpFilters(frontendFilters.filters :+ whitelistFilter :+ sessionIdFilter: _*) diff --git a/src/main/g8/app/filters/SessionIdFilter.scala b/src/main/g8/app/filters/SessionIdFilter.scala index 8a217439..d599c39d 100644 --- a/src/main/g8/app/filters/SessionIdFilter.scala +++ b/src/main/g8/app/filters/SessionIdFilter.scala @@ -4,62 +4,49 @@ import java.util.UUID import akka.stream.Materializer import com.google.inject.Inject -import play.api.http.HeaderNames import play.api.mvc._ +import play.api.mvc.request.{Cell, RequestAttrKey} import uk.gov.hmrc.http.{SessionKeys, HeaderNames => HMRCHeaderNames} import scala.concurrent.{ExecutionContext, Future} -class SessionIdFilter ( - override val mat: Materializer, - uuid: => UUID, - implicit val ec: ExecutionContext - ) extends Filter { +class SessionIdFilter( + override val mat: Materializer, + uuid: => UUID, + sessionCookieBaker: SessionCookieBaker, + implicit val ec: ExecutionContext + ) extends Filter { @Inject - def this(mat: Materializer, ec: ExecutionContext) { - this(mat, UUID.randomUUID(), ec) + def this(mat: Materializer, ec: ExecutionContext, sessionCookieBaker: SessionCookieBaker) { + this(mat, UUID.randomUUID(), sessionCookieBaker, ec) } - override def apply(f: (RequestHeader) => Future[Result])(rh: RequestHeader): Future[Result] = { + override def apply(f: RequestHeader => Future[Result])(rh: RequestHeader): Future[Result] = { lazy val sessionId: String = s"session-\$uuid" if (rh.session.get(SessionKeys.sessionId).isEmpty) { - val cookies: String = { - - val session: Session = - rh.session + (SessionKeys.sessionId -> sessionId) - - val cookies = - rh.cookies ++ Seq(Session.encodeAsCookie(session)) - - Cookies.encodeCookieHeader(cookies.toSeq) - } - val headers = rh.headers.add( - HMRCHeaderNames.xSessionId -> sessionId, - HeaderNames.COOKIE -> cookies + HMRCHeaderNames.xSessionId -> sessionId ) - f(rh.copy(headers = headers)).map { - result => + val session = rh.session + (SessionKeys.sessionId -> sessionId) - val cookies = - Cookies.fromSetCookieHeader(result.header.headers.get(HeaderNames.SET_COOKIE)) + f(rh.withHeaders(headers).addAttr(RequestAttrKey.Session, Cell(session))).map { + result => - val session = Session.decodeFromCookie(cookies.get(Session.COOKIE_NAME)).data - .foldLeft(rh.session) { - case (m, n) => - m + n - } + val updatedSession = if (result.session(rh).get(SessionKeys.sessionId).isDefined) { + result.session(rh) + } else { + result.session(rh) + (SessionKeys.sessionId -> sessionId) + } - result.withSession(session + (SessionKeys.sessionId -> sessionId)) + result.withSession(updatedSession) } } else { f(rh) } } } - diff --git a/src/main/g8/app/handlers/ErrorHandler.scala b/src/main/g8/app/handlers/ErrorHandler.scala index b4580362..f3990523 100644 --- a/src/main/g8/app/handlers/ErrorHandler.scala +++ b/src/main/g8/app/handlers/ErrorHandler.scala @@ -1,20 +1,18 @@ package handlers import javax.inject.{Inject, Singleton} - import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.Request import play.twirl.api.Html -import config.FrontendAppConfig import uk.gov.hmrc.play.bootstrap.http.FrontendErrorHandler +import views.html.ErrorTemplate @Singleton class ErrorHandler @Inject()( - appConfig: FrontendAppConfig, - val messagesApi: MessagesApi + val messagesApi: MessagesApi, + view: ErrorTemplate ) extends FrontendErrorHandler with I18nSupport { override def standardErrorTemplate(pageTitle: String, heading: String, message: String)(implicit rh: Request[_]): Html = - views.html.error_template(pageTitle, heading, message, appConfig) + view(pageTitle, heading, message) } - diff --git a/src/main/g8/app/models/UserAnswers.scala b/src/main/g8/app/models/UserAnswers.scala index b8908091..b8d537fe 100644 --- a/src/main/g8/app/models/UserAnswers.scala +++ b/src/main/g8/app/models/UserAnswers.scala @@ -1,22 +1,21 @@ package models -import uk.gov.hmrc.http.cache.client.CacheMap import pages._ import play.api.libs.json._ -case class UserAnswers(cacheMap: CacheMap) extends Enumerable.Implicits { +case class UserAnswers(userData: UserData) extends Enumerable.Implicits { def get[A](page: QuestionPage[A])(implicit rds: Reads[A]): Option[A] = - cacheMap.getEntry[A](page) + userData.getEntry[A](page) def set[A](page: QuestionPage[A], value: A)(implicit writes: Writes[A]): UserAnswers = { - val updatedAnswers = UserAnswers(cacheMap copy (data = cacheMap.data + (page.toString -> Json.toJson(value)))) + val updatedAnswers = UserAnswers(userData copy (data = userData.data + (page.toString -> Json.toJson(value)))) page.cleanup(Some(value), updatedAnswers) } def remove[A](page: QuestionPage[A]): UserAnswers = { - val updatedAnswers = UserAnswers(cacheMap copy (data = cacheMap.data - page)) + val updatedAnswers = UserAnswers(userData copy (data = userData.data - page)) page.cleanup(None, updatedAnswers) } @@ -25,5 +24,5 @@ case class UserAnswers(cacheMap: CacheMap) extends Enumerable.Implicits { object UserAnswers { def apply(cacheId: String): UserAnswers = - UserAnswers(new CacheMap(cacheId, Map())) + UserAnswers(new UserData(cacheId, Json.obj())) } diff --git a/src/main/g8/app/models/UserData.scala b/src/main/g8/app/models/UserData.scala new file mode 100644 index 00000000..60fe2319 --- /dev/null +++ b/src/main/g8/app/models/UserData.scala @@ -0,0 +1,41 @@ +package models + +import java.time.LocalDateTime + +import play.api.libs.json.{JsObject, OWrites, Reads, __} + +case class UserData( + id: String, + data: JsObject, + lastUpdated: LocalDateTime = LocalDateTime.now + ) { + + def getEntry[T](key: String)(implicit reads: Reads[T]): Option[T] = { + (data \ key).validate[T].asOpt + } +} + +object UserData { + + implicit lazy val reads: Reads[UserData] = { + + import play.api.libs.functional.syntax._ + + ( + (__ \ "_id").read[String] and + (__ \ "data").read[JsObject] and + (__ \ "lastUpdated").read[LocalDateTime] + ) (UserData.apply _) + } + + implicit lazy val writes: OWrites[UserData] = { + + import play.api.libs.functional.syntax._ + + ( + (__ \ "_id").write[String] and + (__ \ "data").write[JsObject] and + (__ \ "lastUpdated").write[LocalDateTime] + ) (unlift(UserData.unapply)) + } +} diff --git a/src/main/g8/app/repositories/SessionRepository.scala b/src/main/g8/app/repositories/SessionRepository.scala index 908d22f8..646c5e0d 100644 --- a/src/main/g8/app/repositories/SessionRepository.scala +++ b/src/main/g8/app/repositories/SessionRepository.scala @@ -1,76 +1,71 @@ package repositories -import javax.inject.{Inject, Singleton} - -import org.joda.time.{DateTime, DateTimeZone} -import play.api.{Configuration, Logger} -import play.api.libs.json.{JsValue, Json} -import play.modules.reactivemongo.MongoDbConnection -import reactivemongo.api.DefaultDB +import java.time.LocalDateTime + +import akka.stream.Materializer +import javax.inject.Inject +import models.UserData +import play.api.Configuration +import play.api.libs.json._ +import play.modules.reactivemongo.ReactiveMongoApi import reactivemongo.api.indexes.{Index, IndexType} -import reactivemongo.bson.{BSONDocument, BSONObjectID} -import reactivemongo.play.json.ImplicitBSONHandlers._ -import uk.gov.hmrc.http.cache.client.CacheMap -import uk.gov.hmrc.mongo.ReactiveRepository -import uk.gov.hmrc.mongo.json.ReactiveMongoFormats +import reactivemongo.bson.BSONDocument +import reactivemongo.play.json.ImplicitBSONHandlers.JsObjectDocumentWriter +import reactivemongo.play.json.collection.JSONCollection -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} -case class DatedCacheMap(id: String, - data: Map[String, JsValue], - lastUpdated: DateTime = DateTime.now(DateTimeZone.UTC)) +class DefaultSessionRepository @Inject()( + mongo: ReactiveMongoApi, + config: Configuration + )(implicit ec: ExecutionContext, m: Materializer) extends SessionRepository { -object DatedCacheMap { - implicit val dateFormat = ReactiveMongoFormats.dateTimeFormats - implicit val formats = Json.format[DatedCacheMap] - def apply(cacheMap: CacheMap): DatedCacheMap = DatedCacheMap(cacheMap.id, cacheMap.data) -} + private val collectionName: String = "user-answers" -class ReactiveMongoRepository(config: Configuration, mongo: () => DefaultDB) - extends ReactiveRepository[DatedCacheMap, BSONObjectID](config.getString("appName").get, mongo, DatedCacheMap.formats) { + private val cacheTtl = config.get[Int]("mongodb.timeToLiveInSeconds") - val fieldName = "lastUpdated" - val createdIndexName = "userAnswersExpiry" - val expireAfterSeconds = "expireAfterSeconds" - val timeToLiveInSeconds: Int = config.getInt("mongodb.timeToLiveInSeconds").get + private def collection: Future[JSONCollection] = + mongo.database.map(_.collection[JSONCollection](collectionName)) - createIndex(fieldName, createdIndexName, timeToLiveInSeconds) + private val lastUpdatedIndex = Index( + key = Seq("lastUpdated" -> IndexType.Ascending), + name = Some("user-answers-last-updated-index"), + options = BSONDocument("expireAfterSeconds" -> cacheTtl) + ) - private def createIndex(field: String, indexName: String, ttl: Int): Future[Boolean] = { - collection.indexesManager.ensure(Index(Seq((field, IndexType.Ascending)), Some(indexName), - options = BSONDocument(expireAfterSeconds -> ttl))) map { - result => { - Logger.debug(s"set [\$indexName] with value \$ttl -> result : \$result") - result - } - } recover { - case e => Logger.error("Failed to set TTL index", e) - false - } - } + val started: Future[Unit] = + collection.flatMap { + _.indexesManager.ensure(lastUpdatedIndex) + }.map(_ => ()) - def upsert(cm: CacheMap): Future[Boolean] = { - val selector = BSONDocument("id" -> cm.id) - val cmDocument = Json.toJson(DatedCacheMap(cm)) - val modifier = BSONDocument("\$set" -> cmDocument) + override def get(id: String): Future[Option[UserData]] = + collection.flatMap(_.find(Json.obj("_id" -> id), None).one[UserData]) - collection.update(selector, modifier, upsert = true).map { lastError => - lastError.ok + override def set(userData: UserData): Future[Boolean] = { + + val selector = Json.obj( + "_id" -> userData.id + ) + + val modifier = Json.obj( + "\$set" -> (userData copy (lastUpdated = LocalDateTime.now)) + ) + + collection.flatMap { + _.update(selector, modifier, upsert = true).map { + lastError => + lastError.ok + } } } - - def get(id: String): Future[Option[CacheMap]] = - collection.find(Json.obj("id" -> id)).one[CacheMap] } -@Singleton -class SessionRepository @Inject()(config: Configuration) { +trait SessionRepository { - class DbConnection extends MongoDbConnection + val started: Future[Unit] - private lazy val sessionRepository = new ReactiveMongoRepository(config, new DbConnection().db) + def get(id: String): Future[Option[UserData]] - def apply(): ReactiveMongoRepository = sessionRepository + def set(userData: UserData): Future[Boolean] } diff --git a/src/main/g8/app/views/check_your_answers.scala.html b/src/main/g8/app/views/CheckYourAnswersView.scala.html similarity index 56% rename from src/main/g8/app/views/check_your_answers.scala.html rename to src/main/g8/app/views/CheckYourAnswersView.scala.html index 79b0c274..4de73781 100644 --- a/src/main/g8/app/views/check_your_answers.scala.html +++ b/src/main/g8/app/views/CheckYourAnswersView.scala.html @@ -1,12 +1,14 @@ -@import config.FrontendAppConfig -@import viewmodels.{Section, AnswerSection, RepeaterAnswerSection} +@import viewmodels.{AnswerSection, RepeaterAnswerSection, Section} -@(appConfig: FrontendAppConfig, answerSections: Seq[Section])(implicit request: Request[_], messages: Messages) +@this( + main_template: MainTemplate +) + +@(answerSections: Seq[Section])(implicit request: Request[_], messages: Messages) @main_template( - title = messages("checkYourAnswers.title"), - appConfig = appConfig, - bodyClasses = None) { + title = messages("checkYourAnswers.title") + ) { @components.heading("checkYourAnswers.heading") diff --git a/src/main/g8/app/views/error_template.scala.html b/src/main/g8/app/views/ErrorTemplate.scala.html similarity index 56% rename from src/main/g8/app/views/error_template.scala.html rename to src/main/g8/app/views/ErrorTemplate.scala.html index f5eb06fe..9b159c78 100644 --- a/src/main/g8/app/views/error_template.scala.html +++ b/src/main/g8/app/views/ErrorTemplate.scala.html @@ -1,6 +1,11 @@ @import config.FrontendAppConfig -@(pageTitle: String, heading: String, message: String, appConfig: FrontendAppConfig)(implicit request: Request[_], messages: Messages) +@this( + govuk_wrapper: GovukWrapper, + appConfig: FrontendAppConfig +) + +@(pageTitle: String, heading: String, message: String)(implicit request: Request[_], messages: Messages) @contentHeader = {

@heading

diff --git a/src/main/g8/app/views/govuk_wrapper.scala.html b/src/main/g8/app/views/GovukWrapper.scala.html similarity index 66% rename from src/main/g8/app/views/govuk_wrapper.scala.html rename to src/main/g8/app/views/GovukWrapper.scala.html index 9b915a8a..7f5f3150 100644 --- a/src/main/g8/app/views/govuk_wrapper.scala.html +++ b/src/main/g8/app/views/GovukWrapper.scala.html @@ -1,3 +1,19 @@ +@import play.twirl.api.HtmlFormat +@import views.html.components.language_selection +@import views.html.layouts.GovUkTemplate + +@this( + header_nav: HeaderNav, + head: Head, + footer: Footer, + serviceInfo: ServiceInfo, + main_content_header: MainContentHeader, + footer_links: FooterLinks, + main_content: MainContent, + reportAProblemLink: ReportAProblemLink, + hmrcGovUkTemplate: GovUkTemplate +) + @(appConfig: config.FrontendAppConfig, title: String, mainClass: Option[String] = None, @@ -9,12 +25,8 @@ serviceInfoContent: Html = HtmlFormat.empty, scriptElem: Option[Html] = None)(implicit request: Request[_], messages: Messages) -@import layouts.{govuk_template => hmrcGovUkTemplate} -@import uk.gov.hmrc.play.views.html.{layouts => uiLayouts} -@import uk.gov.hmrc.play.views.html.{helpers => uiHelpers} - -@head = { - @uiLayouts.head( +@headContent = { + @head( linkElem = None, headScripts = None) @@ -24,7 +36,7 @@ @headerNavLinks = {} @insideHeader = { - @uiLayouts.header_nav( + @header_nav( navTitle = Some(messages("site.service_name")), navTitleLink = None, showBetaLink = false, @@ -34,7 +46,7 @@ @afterHeader = {} @bodyEnd = { - @uiLayouts.footer( + @footer( analyticsToken = Some(appConfig.analyticsToken), analyticsHost = appConfig.analyticsHost, ssoUrl = None, @@ -46,8 +58,8 @@ @footerTop = {} @footerLinks = {} -@serviceInfo = { - @uiLayouts.serviceInfo( +@serviceInfoBlock = { + @serviceInfo( betaBanner = HtmlFormat.empty, includeHMRCBranding = false, includeGridWrapper = false, @@ -57,28 +69,28 @@ @mainContentHeader = { @if(appConfig.languageTranslationEnabled) { - @views.html.language_selection( + @language_selection( appConfig.languageMap, appConfig.routeToSwitchLanguage, Some("translate u-align--right")) } @if(contentHeader.isDefined) { - @uiLayouts.main_content_header(contentHeader = contentHeader.get) + @main_content_header(contentHeader = contentHeader.get) } } -@getHelpForm = @{uiHelpers.reportAProblemLink(appConfig.reportAProblemPartialUrl, appConfig.reportAProblemNonJSUrl)} +@getHelpForm = @{reportAProblemLink(appConfig.reportAProblemPartialUrl, appConfig.reportAProblemNonJSUrl)} @content = { - @uiLayouts.main_content( + @main_content( article = mainContent, mainClass = mainClass, mainDataAttributes = mainDataAttributes, mainContentHeader = mainContentHeader, - serviceInfo = serviceInfo, + serviceInfo = serviceInfoBlock, getHelpForm = getHelpForm, sidebar = sidebar) } -@hmrcGovUkTemplate(Some(title), bodyClasses)(head, bodyEnd, insideHeader, afterHeader, footerTop, Some(uiLayouts.footer_links()), true)(content) +@hmrcGovUkTemplate(Some(title), bodyClasses)(headContent, bodyEnd, insideHeader, afterHeader, footerTop, Some(footer_links()), true)(content) diff --git a/src/main/g8/app/views/IndexView.scala.html b/src/main/g8/app/views/IndexView.scala.html new file mode 100644 index 00000000..656b59b3 --- /dev/null +++ b/src/main/g8/app/views/IndexView.scala.html @@ -0,0 +1,14 @@ +@this( + main_template: MainTemplate +) + +@()(implicit request: Request[_], messages: Messages) + +@main_template( + title = messages("index.title") + ) { + + @components.heading("index.heading") + +

@messages("index.guidance")

+} diff --git a/src/main/g8/app/views/main_template.scala.html b/src/main/g8/app/views/MainTemplate.scala.html similarity index 71% rename from src/main/g8/app/views/main_template.scala.html rename to src/main/g8/app/views/MainTemplate.scala.html index 259aac42..d795db2a 100644 --- a/src/main/g8/app/views/main_template.scala.html +++ b/src/main/g8/app/views/MainTemplate.scala.html @@ -1,7 +1,13 @@ @import config.FrontendAppConfig +@this( + appConfig: FrontendAppConfig, + govuk_wrapper: GovukWrapper, + article: Article, + sidebar: Sidebar +) + @(title: String, - appConfig: FrontendAppConfig, sidebarLinks: Option[Html] = None, contentHeader: Option[Html] = None, bodyClasses: Option[String] = None, @@ -12,9 +18,9 @@ @serviceInfoContent = {} -@sidebar = { +@sidebarContent = { @if(sidebarLinks.isDefined) { - @layouts.sidebar(sidebarLinks.get, Some("sidebar")) + @sidebar(sidebarLinks.get, Some("sidebar")) } } @@ -22,9 +28,9 @@ title = title, mainClass = mainClass, bodyClasses = bodyClasses, - sidebar = sidebar, + sidebar = sidebarContent, contentHeader = contentHeader, - mainContent = layouts.article(mainContent), + mainContent = article(mainContent), serviceInfoContent = serviceInfoContent, scriptElem = scriptElem ) diff --git a/src/main/g8/app/views/SessionExpiredView.scala.html b/src/main/g8/app/views/SessionExpiredView.scala.html new file mode 100644 index 00000000..39e85462 --- /dev/null +++ b/src/main/g8/app/views/SessionExpiredView.scala.html @@ -0,0 +1,14 @@ +@this( + main_template: MainTemplate +) + +@()(implicit request: Request[_], messages: Messages) + +@main_template( + title = messages("session_expired.title") + ) { + + @components.heading("session_expired.heading") + +

@messages("session_expired.guidance")

+} diff --git a/src/main/g8/app/views/UnauthorisedView.scala.html b/src/main/g8/app/views/UnauthorisedView.scala.html new file mode 100644 index 00000000..3c5023e7 --- /dev/null +++ b/src/main/g8/app/views/UnauthorisedView.scala.html @@ -0,0 +1,12 @@ +@this( + main_template: MainTemplate +) + +@()(implicit request: Request[_], messages: Messages) + +@main_template( + title = messages("unauthorised.title") + ) { + + @components.heading("unauthorised.heading") +} diff --git a/src/main/g8/app/views/components/language_selection.scala.html b/src/main/g8/app/views/components/language_selection.scala.html new file mode 100644 index 00000000..4b3d46f3 --- /dev/null +++ b/src/main/g8/app/views/components/language_selection.scala.html @@ -0,0 +1,17 @@ +@(langMap: Map[String, Lang], langToCall: String => Call, customClass: Option[String] = None, appName: Option[String] = None)(implicit messages: Messages) + +

+ +@langMap.map { case (key: String, value: Lang) => + @if(messages.lang.code != value.code) { + + @key.capitalize + + } else { + @key.capitalize + } + @if(key != langMap.last._1) { + @Html(" | ") + } +} +

diff --git a/src/main/g8/app/views/index.scala.html b/src/main/g8/app/views/index.scala.html deleted file mode 100644 index af9eeb8d..00000000 --- a/src/main/g8/app/views/index.scala.html +++ /dev/null @@ -1,13 +0,0 @@ -@import config.FrontendAppConfig - -@(appConfig: FrontendAppConfig)(implicit request: Request[_], messages: Messages) - -@main_template( - title = messages("index.title"), - appConfig = appConfig, - bodyClasses = None) { - - @components.heading("index.heading") - -

@messages("index.guidance")

-} diff --git a/src/main/g8/app/views/session_expired.scala.html b/src/main/g8/app/views/session_expired.scala.html deleted file mode 100644 index 2d69afff..00000000 --- a/src/main/g8/app/views/session_expired.scala.html +++ /dev/null @@ -1,13 +0,0 @@ -@import config.FrontendAppConfig - -@(appConfig: FrontendAppConfig)(implicit request: Request[_], messages: Messages) - -@main_template( - title = messages("session_expired.title"), - appConfig = appConfig, - bodyClasses = None) { - - @components.heading("session_expired.heading") - -

@messages("session_expired.guidance")

-} diff --git a/src/main/g8/app/views/unauthorised.scala.html b/src/main/g8/app/views/unauthorised.scala.html deleted file mode 100644 index b53a6b7c..00000000 --- a/src/main/g8/app/views/unauthorised.scala.html +++ /dev/null @@ -1,12 +0,0 @@ -@import config.FrontendAppConfig - -@(appConfig: FrontendAppConfig)(implicit request: Request[_], messages: Messages) - -@main_template( - title = messages("unauthorised.title"), - appConfig = appConfig, - bodyClasses = None) { - - @components.heading("unauthorised.heading") - -} diff --git a/src/main/g8/build.sbt b/src/main/g8/build.sbt index a2401fbd..6f04308a 100644 --- a/src/main/g8/build.sbt +++ b/src/main/g8/build.sbt @@ -1,4 +1,5 @@ import play.sbt.routes.RoutesKeys +import sbt.Def import scoverage.ScoverageKeys import uk.gov.hmrc.DefaultBuildSettings import uk.gov.hmrc.versioning.SbtGitVersioning.autoImport.majorVersion @@ -10,10 +11,20 @@ lazy val root = (project in file(".")) .settings(DefaultBuildSettings.scalaSettings: _*) .settings(DefaultBuildSettings.defaultSettings(): _*) .settings(SbtDistributablesPlugin.publishingSettings: _*) + .settings(inConfig(Test)(testSettings): _*) .settings(majorVersion := 0) .settings( name := appName, RoutesKeys.routesImport += "models._", + TwirlKeys.templateImports ++= Seq( + "play.twirl.api.HtmlFormat", + "play.twirl.api.HtmlFormat._", + "uk.gov.hmrc.play.views.html.helpers._", + "uk.gov.hmrc.play.views.html.layouts._", + "views.ViewUtils._", + "models.Mode", + "controllers.routes._" + ), PlayKeys.playDefaultPort := $port$, ScoverageKeys.coverageExcludedFiles := ";Reverse.*;.*filters.*;.*handlers.*;.*components.*;.*repositories.*;" + ".*BuildInfo.*;.*javascript.*;.*FrontendAuditConnector.*;.*Routes.*;.*GuiceInjector;" + @@ -21,12 +32,11 @@ lazy val root = (project in file(".")) ScoverageKeys.coverageMinimum := 80, ScoverageKeys.coverageFailOnMinimum := true, ScoverageKeys.coverageHighlighting := true, - scalacOptions ++= Seq("-Xfatal-warnings", "-feature"), + scalacOptions ++= Seq("-feature"), libraryDependencies ++= AppDependencies(), retrieveManaged := true, evictionWarningOptions in update := EvictionWarningOptions.default.withWarnScalaVersionEviction(false), - fork in Test := true, resolvers ++= Seq( Resolver.bintrayRepo("hmrc", "releases"), Resolver.jcenterRepo @@ -37,10 +47,17 @@ lazy val root = (project in file(".")) group(Seq("javascripts/show-hide-content.js", "javascripts/$name;format="word"$.js")) ), // prevent removal of unused code which generates warning errors due to use of third-party libs - UglifyKeys.compressOptions := Seq("unused=false", "dead_code=false"), + uglifyCompressOptions := Seq("unused=false", "dead_code=false"), pipelineStages := Seq(digest), // below line required to force asset pipeline to operate in dev rather than only prod pipelineStages in Assets := Seq(concat,uglify), // only compress files generated by concat includeFilter in uglify := GlobFilter("$name;format="word"$-*.js") ) + +lazy val testSettings: Seq[Def.Setting[_]] = Seq( + fork := true, + javaOptions ++= Seq( + "-Dconfig.resource=test.application.conf" + ) +) diff --git a/src/main/g8/conf/application.conf b/src/main/g8/conf/application.conf index 69be9819..92460512 100644 --- a/src/main/g8/conf/application.conf +++ b/src/main/g8/conf/application.conf @@ -1,42 +1,31 @@ -include "common.conf" - -# Secret key -# ~~~~~ -# The secret key is used to secure cryptographics functions. -# If you deploy your application to several instances be sure to use the same key! - -# this key is for local development only! -play.crypto.secret="yNhI04vHs9<_HWbC`]20u`37=NGLGYY5:0Tg5?y`W eqTo, _} -import org.scalacheck.Arbitrary.arbitrary -import org.scalacheck.Gen -import org.scalatest.concurrent.ScalaFutures -import org.scalatest.mockito.MockitoSugar -import org.scalatest.prop.PropertyChecks -import org.scalatest.{MustMatchers, OptionValues, WordSpec} -import play.api.libs.json.{JsBoolean, JsNumber, JsString} -import repositories.{ReactiveMongoRepository, SessionRepository} -import uk.gov.hmrc.http.cache.client.CacheMap - -import scala.concurrent.Future -import scala.concurrent.ExecutionContext.Implicits.global - -class MongoCacheConnectorSpec - extends WordSpec with MustMatchers with PropertyChecks with Generators with MockitoSugar with ScalaFutures with OptionValues { - - ".save" must { - - "save the cache map to the Mongo repository" in { - - val mockReactiveMongoRepository = mock[ReactiveMongoRepository] - val mockSessionRepository = mock[SessionRepository] - - when(mockSessionRepository.apply()) thenReturn mockReactiveMongoRepository - when(mockReactiveMongoRepository.upsert(any[CacheMap])) thenReturn Future.successful(true) - - val mongoCacheConnector = new MongoCacheConnector(mockSessionRepository) - - forAll(arbitrary[CacheMap]) { - cacheMap => - - val result = mongoCacheConnector.save(cacheMap) - - whenReady(result) { - savedCacheMap => - - savedCacheMap mustEqual cacheMap - verify(mockReactiveMongoRepository).upsert(cacheMap) - } - } - } - } - - ".fetch" when { - - "there isn't a record for this key in Mongo" must { - - "return None" in { - - val mockReactiveMongoRepository = mock[ReactiveMongoRepository] - val mockSessionRepository = mock[SessionRepository] - - when(mockSessionRepository.apply()) thenReturn mockReactiveMongoRepository - when(mockReactiveMongoRepository.get(any())) thenReturn Future.successful(None) - - val mongoCacheConnector = new MongoCacheConnector(mockSessionRepository) - - forAll(nonEmptyString) { - cacheId => - - val result = mongoCacheConnector.fetch(cacheId) - - whenReady(result) { - optionalCacheMap => - - optionalCacheMap must be(empty) - } - } - } - } - - "a record exists for this key" must { - - "return the record" in { - - val mockReactiveMongoRepository = mock[ReactiveMongoRepository] - val mockSessionRepository = mock[SessionRepository] - - when(mockSessionRepository.apply()) thenReturn mockReactiveMongoRepository - - val mongoCacheConnector = new MongoCacheConnector(mockSessionRepository) - - forAll(arbitrary[CacheMap]) { - cacheMap => - - when(mockReactiveMongoRepository.get(eqTo(cacheMap.id))) thenReturn Future.successful(Some(cacheMap)) - - val result = mongoCacheConnector.fetch(cacheMap.id) - - whenReady(result) { - optionalCacheMap => - - optionalCacheMap.value mustEqual cacheMap - } - } - } - } - } - - ".getEntry" when { - - "there isn't a record for this key in Mongo" must { - - "return None" in { - - val mockReactiveMongoRepository = mock[ReactiveMongoRepository] - val mockSessionRepository = mock[SessionRepository] - - when(mockSessionRepository.apply()) thenReturn mockReactiveMongoRepository - when(mockReactiveMongoRepository.get(any())) thenReturn Future.successful(None) - - val mongoCacheConnector = new MongoCacheConnector(mockSessionRepository) - - forAll(nonEmptyString, nonEmptyString) { - (cacheId, key) => - - val result = mongoCacheConnector.getEntry[String](cacheId, key) - - whenReady(result) { - optionalValue => - - optionalValue must be(empty) - } - } - } - } - - "a record exists in Mongo but this key is not present" must { - - "return None" in { - - val mockReactiveMongoRepository = mock[ReactiveMongoRepository] - val mockSessionRepository = mock[SessionRepository] - - when(mockSessionRepository.apply()) thenReturn mockReactiveMongoRepository - - val mongoCacheConnector = new MongoCacheConnector(mockSessionRepository) - - val gen = for { - key <- nonEmptyString - cacheMap <- arbitrary[CacheMap] - } yield (key, cacheMap copy (data = cacheMap.data - key)) - - forAll(gen) { - case (key, cacheMap) => - - when(mockReactiveMongoRepository.get(eqTo(cacheMap.id))) thenReturn Future.successful(Some(cacheMap)) - - val result = mongoCacheConnector.getEntry[String](cacheMap.id, key) - - whenReady(result) { - optionalValue => - - optionalValue must be(empty) - } - } - } - } - - "a record exists in Mongo with this key" must { - - "return the key's value" in { - - val mockReactiveMongoRepository = mock[ReactiveMongoRepository] - val mockSessionRepository = mock[SessionRepository] - - when(mockSessionRepository.apply()) thenReturn mockReactiveMongoRepository - - val mongoCacheConnector = new MongoCacheConnector(mockSessionRepository) - - val gen = for { - key <- nonEmptyString - value <- nonEmptyString - cacheMap <- arbitrary[CacheMap] - } yield (key, value, cacheMap copy (data = cacheMap.data + (key -> JsString(value)))) - - forAll(gen) { - case (key, value, cacheMap) => - - when(mockReactiveMongoRepository.get(eqTo(cacheMap.id))) thenReturn Future.successful(Some(cacheMap)) - - val result = mongoCacheConnector.getEntry[String](cacheMap.id, key) - - whenReady(result) { - optionalValue => - - optionalValue.value mustEqual value - } - } - } - } - } -} diff --git a/src/main/g8/test/controllers/CheckYourAnswersControllerSpec.scala b/src/main/g8/test/controllers/CheckYourAnswersControllerSpec.scala index 5a595e9c..32bbcc09 100644 --- a/src/main/g8/test/controllers/CheckYourAnswersControllerSpec.scala +++ b/src/main/g8/test/controllers/CheckYourAnswersControllerSpec.scala @@ -1,27 +1,42 @@ package controllers +import base.SpecBase +import play.api.test.FakeRequest import play.api.test.Helpers._ -import controllers.actions.{DataRequiredActionImpl, DataRetrievalAction, FakeIdentifierAction} import viewmodels.AnswerSection -import views.html.check_your_answers +import views.html.CheckYourAnswersView -class CheckYourAnswersControllerSpec extends ControllerSpecBase { - - def controller(dataRetrievalAction: DataRetrievalAction = getEmptyCacheMap) = - new CheckYourAnswersController(frontendAppConfig, messagesApi, FakeIdentifierAction, dataRetrievalAction, new DataRequiredActionImpl) +class CheckYourAnswersControllerSpec extends SpecBase { "Check Your Answers Controller" must { - "return 200 and the correct view for a GET" in { - val result = controller().onPageLoad()(fakeRequest) - status(result) mustBe OK - contentAsString(result) mustBe check_your_answers(frontendAppConfig, Seq(AnswerSection(None, Seq())))(fakeRequest, messages).toString + + "return OK and the correct view for a GET" in { + + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val request = FakeRequest(GET, routes.CheckYourAnswersController.onPageLoad().url) + + val result = route(application, request).value + + val view = application.injector.instanceOf[CheckYourAnswersView] + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view(Seq(AnswerSection(None, Seq())))(fakeRequest, messages).toString } "redirect to Session Expired for a GET if no existing data is found" in { - val result = controller(dontGetAnyData).onPageLoad()(fakeRequest) - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(routes.SessionExpiredController.onPageLoad().url) + val application = applicationBuilder(userData = None).build() + + val request = FakeRequest(GET, routes.CheckYourAnswersController.onPageLoad().url) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url } } } diff --git a/src/main/g8/test/controllers/ControllerSpecBase.scala b/src/main/g8/test/controllers/ControllerSpecBase.scala deleted file mode 100644 index a65de6c2..00000000 --- a/src/main/g8/test/controllers/ControllerSpecBase.scala +++ /dev/null @@ -1,16 +0,0 @@ -package controllers - -import uk.gov.hmrc.http.cache.client.CacheMap -import base.SpecBase -import controllers.actions.FakeDataRetrievalAction - -trait ControllerSpecBase extends SpecBase { - - val cacheMapId = "id" - - def emptyCacheMap = CacheMap(cacheMapId, Map()) - - def getEmptyCacheMap = new FakeDataRetrievalAction(Some(emptyCacheMap)) - - def dontGetAnyData = new FakeDataRetrievalAction(None) -} diff --git a/src/main/g8/test/controllers/IndexControllerSpec.scala b/src/main/g8/test/controllers/IndexControllerSpec.scala index 355e84cf..384e3858 100644 --- a/src/main/g8/test/controllers/IndexControllerSpec.scala +++ b/src/main/g8/test/controllers/IndexControllerSpec.scala @@ -1,19 +1,28 @@ package controllers +import base.SpecBase +import play.api.test.FakeRequest import play.api.test.Helpers._ -import views.html.index +import views.html.IndexView -class IndexControllerSpec extends ControllerSpecBase { +class IndexControllerSpec extends SpecBase { "Index Controller" must { - "return 200 for a GET" in { - val result = new IndexController(frontendAppConfig, messagesApi).onPageLoad()(fakeRequest) - status(result) mustBe OK - } - "return the correct view for a GET" in { - val result = new IndexController(frontendAppConfig, messagesApi).onPageLoad()(fakeRequest) - contentAsString(result) mustBe index(frontendAppConfig)(fakeRequest, messages).toString + "return OK and the correct view for a GET" in { + + val application = applicationBuilder(userData = None).build() + + val request = FakeRequest(GET, routes.IndexController.onPageLoad().url) + + val result = route(application, request).value + + val view = application.injector.instanceOf[IndexView] + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view()(fakeRequest, messages).toString } } } diff --git a/src/main/g8/test/controllers/SessionExpiredControllerSpec.scala b/src/main/g8/test/controllers/SessionExpiredControllerSpec.scala index 1382e0f4..e88ae82f 100644 --- a/src/main/g8/test/controllers/SessionExpiredControllerSpec.scala +++ b/src/main/g8/test/controllers/SessionExpiredControllerSpec.scala @@ -1,19 +1,28 @@ package controllers +import base.SpecBase +import play.api.test.FakeRequest import play.api.test.Helpers._ -import views.html.session_expired +import views.html.SessionExpiredView -class SessionExpiredControllerSpec extends ControllerSpecBase { +class SessionExpiredControllerSpec extends SpecBase { "SessionExpired Controller" must { - "return 200 for a GET" in { - val result = new SessionExpiredController(frontendAppConfig, messagesApi).onPageLoad()(fakeRequest) - status(result) mustBe OK - } - "return the correct view for a GET" in { - val result = new SessionExpiredController(frontendAppConfig, messagesApi).onPageLoad()(fakeRequest) - contentAsString(result) mustBe session_expired(frontendAppConfig)(fakeRequest, messages).toString + "return OK and the correct view for a GET" in { + + val application = applicationBuilder(userData = None).build() + + val request = FakeRequest(GET, routes.SessionExpiredController.onPageLoad().url) + + val result = route(application, request).value + + val view = application.injector.instanceOf[SessionExpiredView] + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view()(fakeRequest, messages).toString } } } diff --git a/src/main/g8/test/controllers/UnauthorisedControllerSpec.scala b/src/main/g8/test/controllers/UnauthorisedControllerSpec.scala index b2bc35b4..5121b5ff 100644 --- a/src/main/g8/test/controllers/UnauthorisedControllerSpec.scala +++ b/src/main/g8/test/controllers/UnauthorisedControllerSpec.scala @@ -1,19 +1,28 @@ package controllers +import base.SpecBase +import play.api.test.FakeRequest import play.api.test.Helpers._ -import views.html.unauthorised +import views.html.UnauthorisedView -class UnauthorisedControllerSpec extends ControllerSpecBase { +class UnauthorisedControllerSpec extends SpecBase { "Unauthorised Controller" must { - "return 200 for a GET" in { - val result = new UnauthorisedController(frontendAppConfig, messagesApi).onPageLoad()(fakeRequest) - status(result) mustBe OK - } - "return the correct view for a GET" in { - val result = new UnauthorisedController(frontendAppConfig, messagesApi).onPageLoad()(fakeRequest) - contentAsString(result) mustBe unauthorised(frontendAppConfig)(fakeRequest, messages).toString + "return OK and the correct view for a GET" in { + + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val request = FakeRequest(GET, routes.UnauthorisedController.onPageLoad().url) + + val result = route(application, request).value + + val view = application.injector.instanceOf[UnauthorisedView] + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view()(fakeRequest, messages).toString } } } diff --git a/src/main/g8/test/controllers/actions/AuthActionSpec.scala b/src/main/g8/test/controllers/actions/AuthActionSpec.scala index 62df04d8..e0008c3c 100644 --- a/src/main/g8/test/controllers/actions/AuthActionSpec.scala +++ b/src/main/g8/test/controllers/actions/AuthActionSpec.scala @@ -1,97 +1,155 @@ package controllers.actions -import play.api.mvc.Controller +import base.SpecBase +import com.google.inject.Inject +import controllers.routes +import play.api.mvc.{BodyParsers, Results} import play.api.test.Helpers._ import uk.gov.hmrc.auth.core._ import uk.gov.hmrc.auth.core.authorise.Predicate import uk.gov.hmrc.auth.core.retrieve.Retrieval -import base.SpecBase -import controllers.routes import uk.gov.hmrc.http.HeaderCarrier -import scala.concurrent.{ExecutionContext, Future} import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.{ExecutionContext, Future} class AuthActionSpec extends SpecBase { - class Harness(authAction: IdentifierAction) extends Controller { - def onPageLoad() = authAction { request => Ok } + class Harness(authAction: IdentifierAction) { + def onPageLoad() = authAction { _ => Results.Ok } } "Auth Action" when { + "the user hasn't logged in" must { + "redirect the user to log in " in { - val authAction = new AuthenticatedIdentifierAction(new FakeFailingAuthConnector(new MissingBearerToken), frontendAppConfig) + + val application = applicationBuilder(userData = None).build() + + val bodyParsers = application.injector.instanceOf[BodyParsers.Default] + + val authAction = new AuthenticatedIdentifierAction(new FakeFailingAuthConnector(new MissingBearerToken), frontendAppConfig, bodyParsers) val controller = new Harness(authAction) val result = controller.onPageLoad()(fakeRequest) + status(result) mustBe SEE_OTHER + redirectLocation(result).get must startWith(frontendAppConfig.loginUrl) } } "the user's session has expired" must { + "redirect the user to log in " in { - val authAction = new AuthenticatedIdentifierAction(new FakeFailingAuthConnector(new BearerTokenExpired), frontendAppConfig) + + val application = applicationBuilder(userData = None).build() + + val bodyParsers = application.injector.instanceOf[BodyParsers.Default] + + val authAction = new AuthenticatedIdentifierAction(new FakeFailingAuthConnector(new BearerTokenExpired), frontendAppConfig, bodyParsers) val controller = new Harness(authAction) val result = controller.onPageLoad()(fakeRequest) + status(result) mustBe SEE_OTHER + redirectLocation(result).get must startWith(frontendAppConfig.loginUrl) } } "the user doesn't have sufficient enrolments" must { + "redirect the user to the unauthorised page" in { - val authAction = new AuthenticatedIdentifierAction(new FakeFailingAuthConnector(new InsufficientEnrolments), frontendAppConfig) + + val application = applicationBuilder(userData = None).build() + + val bodyParsers = application.injector.instanceOf[BodyParsers.Default] + + val authAction = new AuthenticatedIdentifierAction(new FakeFailingAuthConnector(new InsufficientEnrolments), frontendAppConfig, bodyParsers) val controller = new Harness(authAction) val result = controller.onPageLoad()(fakeRequest) + status(result) mustBe SEE_OTHER + redirectLocation(result) mustBe Some(routes.UnauthorisedController.onPageLoad().url) } } "the user doesn't have sufficient confidence level" must { + "redirect the user to the unauthorised page" in { - val authAction = new AuthenticatedIdentifierAction(new FakeFailingAuthConnector(new InsufficientConfidenceLevel), frontendAppConfig) + + val application = applicationBuilder(userData = None).build() + + val bodyParsers = application.injector.instanceOf[BodyParsers.Default] + + val authAction = new AuthenticatedIdentifierAction(new FakeFailingAuthConnector(new InsufficientConfidenceLevel), frontendAppConfig, bodyParsers) val controller = new Harness(authAction) val result = controller.onPageLoad()(fakeRequest) + status(result) mustBe SEE_OTHER + redirectLocation(result) mustBe Some(routes.UnauthorisedController.onPageLoad().url) } } "the user used an unaccepted auth provider" must { + "redirect the user to the unauthorised page" in { - val authAction = new AuthenticatedIdentifierAction(new FakeFailingAuthConnector(new UnsupportedAuthProvider), frontendAppConfig) + + val application = applicationBuilder(userData = None).build() + + val bodyParsers = application.injector.instanceOf[BodyParsers.Default] + + val authAction = new AuthenticatedIdentifierAction(new FakeFailingAuthConnector(new UnsupportedAuthProvider), frontendAppConfig, bodyParsers) val controller = new Harness(authAction) val result = controller.onPageLoad()(fakeRequest) + status(result) mustBe SEE_OTHER + redirectLocation(result) mustBe Some(routes.UnauthorisedController.onPageLoad().url) } } "the user has an unsupported affinity group" must { + "redirect the user to the unauthorised page" in { - val authAction = new AuthenticatedIdentifierAction(new FakeFailingAuthConnector(new UnsupportedAffinityGroup), frontendAppConfig) + + val application = applicationBuilder(userData = None).build() + + val bodyParsers = application.injector.instanceOf[BodyParsers.Default] + + val authAction = new AuthenticatedIdentifierAction(new FakeFailingAuthConnector(new UnsupportedAffinityGroup), frontendAppConfig, bodyParsers) val controller = new Harness(authAction) val result = controller.onPageLoad()(fakeRequest) + status(result) mustBe SEE_OTHER + redirectLocation(result) mustBe Some(routes.UnauthorisedController.onPageLoad().url) } } "the user has an unsupported credential role" must { + "redirect the user to the unauthorised page" in { - val authAction = new AuthenticatedIdentifierAction(new FakeFailingAuthConnector(new UnsupportedCredentialRole), frontendAppConfig) + + val application = applicationBuilder(userData = None).build() + + val bodyParsers = application.injector.instanceOf[BodyParsers.Default] + + val authAction = new AuthenticatedIdentifierAction(new FakeFailingAuthConnector(new UnsupportedCredentialRole), frontendAppConfig, bodyParsers) val controller = new Harness(authAction) val result = controller.onPageLoad()(fakeRequest) + status(result) mustBe SEE_OTHER + redirectLocation(result) mustBe Some(routes.UnauthorisedController.onPageLoad().url) } } } } -class FakeFailingAuthConnector(exceptionToReturn: Throwable) extends AuthConnector { +class FakeFailingAuthConnector @Inject()(exceptionToReturn: Throwable) extends AuthConnector { val serviceUrl: String = "" override def authorise[A](predicate: Predicate, retrieval: Retrieval[A])(implicit hc: HeaderCarrier, ec: ExecutionContext): Future[A] = diff --git a/src/main/g8/test/controllers/actions/DataRetrievalActionSpec.scala b/src/main/g8/test/controllers/actions/DataRetrievalActionSpec.scala index 42418de9..975a529e 100644 --- a/src/main/g8/test/controllers/actions/DataRetrievalActionSpec.scala +++ b/src/main/g8/test/controllers/actions/DataRetrievalActionSpec.scala @@ -1,28 +1,32 @@ package controllers.actions +import base.SpecBase +import models.UserData +import models.requests.{IdentifierRequest, OptionalDataRequest} import org.mockito.Mockito._ import org.scalatest.concurrent.ScalaFutures import org.scalatest.mockito.MockitoSugar -import base.SpecBase -import connectors.DataCacheConnector -import models.requests.{IdentifierRequest, OptionalDataRequest} -import uk.gov.hmrc.http.cache.client.CacheMap +import play.api.libs.json.Json +import repositories.SessionRepository -import scala.concurrent.Future import scala.concurrent.ExecutionContext.Implicits.global +import scala.concurrent.Future class DataRetrievalActionSpec extends SpecBase with MockitoSugar with ScalaFutures { - class Harness(dataCacheConnector: DataCacheConnector) extends DataRetrievalActionImpl(dataCacheConnector) { + class Harness(sessionRepository: SessionRepository) extends DataRetrievalActionImpl(sessionRepository) { def callTransform[A](request: IdentifierRequest[A]): Future[OptionalDataRequest[A]] = transform(request) } "Data Retrieval Action" when { + "there is no data in the cache" must { + "set userAnswers to 'None' in the request" in { - val dataCacheConnector = mock[DataCacheConnector] - when(dataCacheConnector.fetch("id")) thenReturn Future(None) - val action = new Harness(dataCacheConnector) + + val sessionRepository = mock[SessionRepository] + when(sessionRepository.get("id")) thenReturn Future(None) + val action = new Harness(sessionRepository) val futureResult = action.callTransform(new IdentifierRequest(fakeRequest, "id")) @@ -33,10 +37,12 @@ class DataRetrievalActionSpec extends SpecBase with MockitoSugar with ScalaFutur } "there is data in the cache" must { + "build a userAnswers object and add it to the request" in { - val dataCacheConnector = mock[DataCacheConnector] - when(dataCacheConnector.fetch("id")) thenReturn Future(Some(new CacheMap("id", Map()))) - val action = new Harness(dataCacheConnector) + + val sessionRepository = mock[SessionRepository] + when(sessionRepository.get("id")) thenReturn Future(Some(new UserData("id", Json.obj()))) + val action = new Harness(sessionRepository) val futureResult = action.callTransform(new IdentifierRequest(fakeRequest, "id")) diff --git a/src/main/g8/test/controllers/actions/FakeDataRetrievalAction.scala b/src/main/g8/test/controllers/actions/FakeDataRetrievalAction.scala index 7408fc25..37c04f9e 100644 --- a/src/main/g8/test/controllers/actions/FakeDataRetrievalAction.scala +++ b/src/main/g8/test/controllers/actions/FakeDataRetrievalAction.scala @@ -1,16 +1,20 @@ package controllers.actions -import uk.gov.hmrc.http.cache.client.CacheMap import models.requests.{IdentifierRequest, OptionalDataRequest} -import models.UserAnswers +import models.{UserAnswers, UserData} -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.ExecutionContext.Implicits.global +class FakeDataRetrievalAction(dataToReturn: Option[UserData]) extends DataRetrievalAction { -class FakeDataRetrievalAction(cacheMapToReturn: Option[CacheMap]) extends DataRetrievalAction { - override protected def transform[A](request: IdentifierRequest[A]): Future[OptionalDataRequest[A]] = cacheMapToReturn match { - case None => Future(OptionalDataRequest(request.request, request.identifier, None)) - case Some(cacheMap)=> Future(OptionalDataRequest(request.request, request.identifier, Some(new UserAnswers(cacheMap)))) - } + override protected def transform[A](request: IdentifierRequest[A]): Future[OptionalDataRequest[A]] = + dataToReturn match { + case None => + Future(OptionalDataRequest(request.request, request.identifier, None)) + case Some(userData) => + Future(OptionalDataRequest(request.request, request.identifier, Some(new UserAnswers(userData)))) + } + + override protected implicit val executionContext: ExecutionContext = + scala.concurrent.ExecutionContext.Implicits.global } diff --git a/src/main/g8/test/controllers/actions/FakeIdentifierAction.scala b/src/main/g8/test/controllers/actions/FakeIdentifierAction.scala index e10c7664..728483da 100644 --- a/src/main/g8/test/controllers/actions/FakeIdentifierAction.scala +++ b/src/main/g8/test/controllers/actions/FakeIdentifierAction.scala @@ -1,12 +1,19 @@ package controllers.actions -import play.api.mvc.{Request, Result} +import javax.inject.Inject import models.requests.IdentifierRequest +import play.api.mvc._ -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} -object FakeIdentifierAction extends IdentifierAction { - override def invokeBlock[A](request: Request[A], block: (IdentifierRequest[A]) => Future[Result]): Future[Result] = +class FakeIdentifierAction @Inject()(bodyParsers: PlayBodyParsers) extends IdentifierAction { + + override def invokeBlock[A](request: Request[A], block: IdentifierRequest[A] => Future[Result]): Future[Result] = block(IdentifierRequest(request, "id")) -} + override def parser: BodyParser[AnyContent] = + bodyParsers.default + + override protected def executionContext: ExecutionContext = + scala.concurrent.ExecutionContext.Implicits.global +} diff --git a/src/main/g8/test/controllers/actions/SessionActionSpec.scala b/src/main/g8/test/controllers/actions/SessionActionSpec.scala index e79de9fd..dd006aea 100644 --- a/src/main/g8/test/controllers/actions/SessionActionSpec.scala +++ b/src/main/g8/test/controllers/actions/SessionActionSpec.scala @@ -1,23 +1,7 @@ -/* - * Copyright 2018 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 controllers.actions import base.SpecBase -import play.api.mvc.Controller +import play.api.mvc.{BodyParsers, Results} import play.api.test.Helpers._ import uk.gov.hmrc.http.SessionKeys @@ -25,27 +9,48 @@ import scala.concurrent.ExecutionContext.Implicits.global class SessionActionSpec extends SpecBase { - class Harness(action: IdentifierAction) extends Controller { - def onPageLoad() = action { request => Ok } + class Harness(action: IdentifierAction) { + def onPageLoad() = action { request => Results.Ok } } "Session Action" when { + "there's no active session" must { + "redirect to the session expired page" in { - val sessionAction = new SessionIdentifierAction(frontendAppConfig) + + val application = applicationBuilder(userData = None).build() + + val bodyParsers = application.injector.instanceOf[BodyParsers.Default] + + val sessionAction = new SessionIdentifierAction(frontendAppConfig, bodyParsers) + val controller = new Harness(sessionAction) + val result = controller.onPageLoad()(fakeRequest) + status(result) mustBe SEE_OTHER redirectLocation(result).get must startWith(controllers.routes.SessionExpiredController.onPageLoad().url) } } + "there's an active session" must { + "perform the action" in { - val sessionAction = new SessionIdentifierAction(frontendAppConfig) + + val application = applicationBuilder(userData = None).build() + + val bodyParsers = application.injector.instanceOf[BodyParsers.Default] + + val sessionAction = new SessionIdentifierAction(frontendAppConfig, bodyParsers) + val controller = new Harness(sessionAction) + val request = fakeRequest.withSession(SessionKeys.sessionId -> "foo") + val result = controller.onPageLoad()(request) - status(result) mustBe 200 + + status(result) mustBe OK } } } diff --git a/src/main/g8/test/filters/SessionIdFilterSpec.scala b/src/main/g8/test/filters/SessionIdFilterSpec.scala index 9230172d..efed1bd4 100644 --- a/src/main/g8/test/filters/SessionIdFilterSpec.scala +++ b/src/main/g8/test/filters/SessionIdFilterSpec.scala @@ -4,13 +4,12 @@ import java.util.UUID import akka.stream.Materializer import com.google.inject.Inject -import org.scalatest.{MustMatchers, WordSpec} +import org.scalatest.{MustMatchers, OptionValues, WordSpec} import org.scalatestplus.play.OneAppPerSuite import play.api.Application -import play.api.http.{DefaultHttpFilters, HttpFilters} import play.api.inject.guice.GuiceApplicationBuilder import play.api.libs.json.Json -import play.api.mvc.{Action, Results} +import play.api.mvc.{Action, Results, SessionCookieBaker} import play.api.routing.Router import play.api.test.FakeRequest import play.api.test.Helpers._ @@ -22,15 +21,15 @@ object SessionIdFilterSpec { val sessionId = "28836767-a008-46be-ac18-695ab140e705" - class Filters @Inject() (sessionId: SessionIdFilter) extends DefaultHttpFilters(sessionId) + class TestSessionIdFilter @Inject()( + override val mat: Materializer, + sessionCookieBaker: SessionCookieBaker, + ec: ExecutionContext + ) extends SessionIdFilter(mat, UUID.fromString(sessionId), sessionCookieBaker, ec) - class TestSessionIdFilter @Inject() ( - override val mat: Materializer, - ec: ExecutionContext - ) extends SessionIdFilter(mat, UUID.fromString(sessionId), ec) } -class SessionIdFilterSpec extends WordSpec with MustMatchers with OneAppPerSuite { +class SessionIdFilterSpec extends WordSpec with MustMatchers with OneAppPerSuite with OptionValues { import SessionIdFilterSpec._ @@ -63,28 +62,32 @@ class SessionIdFilterSpec extends WordSpec with MustMatchers with OneAppPerSuite new GuiceApplicationBuilder() .overrides( - bind[HttpFilters].to[Filters], bind[SessionIdFilter].to[TestSessionIdFilter] ) + .configure( + "play.filters.disabled" -> List("uk.gov.hmrc.play.bootstrap.filters.frontend.crypto.SessionCookieCryptoFilter") + ) .router(router) .build() } - ".apply" must { + "session id filter" must { "add a sessionId if one doesn't already exist" in { - val Some(result) = route(app, FakeRequest(GET, "/test")) + val result = route(app, FakeRequest(GET, "/test")).value val body = contentAsJson(result) (body \ "fromHeader").as[String] mustEqual s"session-\$sessionId" (body \ "fromSession").as[String] mustEqual s"session-\$sessionId" + + session(result).data.get(SessionKeys.sessionId) mustBe defined } "not override a sessionId if one doesn't already exist" in { - val Some(result) = route(app, FakeRequest(GET, "/test").withSession(SessionKeys.sessionId -> "foo")) + val result = route(app, FakeRequest(GET, "/test").withSession(SessionKeys.sessionId -> "foo")).value val body = contentAsJson(result) @@ -94,13 +97,14 @@ class SessionIdFilterSpec extends WordSpec with MustMatchers with OneAppPerSuite "not override other session values from the response" in { - val Some(result) = route(app, FakeRequest(GET, "/test2")) + val result = route(app, FakeRequest(GET, "/test2")).value + session(result).data must contain("foo" -> "bar") } "not override other session values from the request" in { - val Some(result) = route(app, FakeRequest(GET, "/test").withSession("foo" -> "bar")) + val result = route(app, FakeRequest(GET, "/test").withSession("foo" -> "bar")).value session(result).data must contain("foo" -> "bar") } } diff --git a/src/main/g8/test/generators/Generators.scala b/src/main/g8/test/generators/Generators.scala index f4bfb6ba..49ff95f2 100644 --- a/src/main/g8/test/generators/Generators.scala +++ b/src/main/g8/test/generators/Generators.scala @@ -4,9 +4,8 @@ import org.scalacheck.{Arbitrary, Gen, Shrink} import Gen._ import Arbitrary._ import play.api.libs.json.{JsBoolean, JsNumber, JsString} -import uk.gov.hmrc.http.cache.client.CacheMap -trait Generators extends CacheMapGenerator with PageGenerators with ModelGenerators with UserAnswersEntryGenerators { +trait Generators extends UserDataGenerator with PageGenerators with ModelGenerators with UserAnswersEntryGenerators { implicit val dontShrink: Shrink[String] = Shrink.shrinkAny diff --git a/src/main/g8/test/generators/CacheMapGenerator.scala b/src/main/g8/test/generators/UserDataGenerator.scala similarity index 63% rename from src/main/g8/test/generators/CacheMapGenerator.scala rename to src/main/g8/test/generators/UserDataGenerator.scala index 632d51f7..86cdb204 100644 --- a/src/main/g8/test/generators/CacheMapGenerator.scala +++ b/src/main/g8/test/generators/UserDataGenerator.scala @@ -1,18 +1,18 @@ package generators +import models.UserData import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.{Arbitrary, Gen} import pages._ -import play.api.libs.json.JsValue -import uk.gov.hmrc.http.cache.client.CacheMap +import play.api.libs.json.{JsValue, Json} -trait CacheMapGenerator { +trait UserDataGenerator { self: Generators => val generators: Seq[Gen[(Page, JsValue)]] = Nil - implicit lazy val arbitraryCacheMap: Arbitrary[CacheMap] = + implicit lazy val arbitraryUserData: Arbitrary[UserData] = Arbitrary { for { cacheId <- nonEmptyString @@ -20,11 +20,11 @@ trait CacheMapGenerator { case Nil => Gen.const(Map[Page, JsValue]()) case _ => Gen.mapOf(oneOf(generators)) } - } yield CacheMap( + } yield UserData( cacheId, data.map { - case (k, v) => ( k.toString, v ) - } + case (k, v) => Json.obj(k.toString -> v) + }.foldLeft(Json.obj())(_ ++ _) ) } } diff --git a/src/main/g8/test/models/EnumerableSpec.scala b/src/main/g8/test/models/EnumerableSpec.scala index eb40d630..e663d8a8 100644 --- a/src/main/g8/test/models/EnumerableSpec.scala +++ b/src/main/g8/test/models/EnumerableSpec.scala @@ -1,7 +1,6 @@ package models import org.scalatest.{EitherValues, OptionValues, MustMatchers, WordSpec} -import play.api.data.validation.ValidationError import play.api.libs.json._ object EnumerableSpec { @@ -37,7 +36,7 @@ class EnumerableSpec extends WordSpec with MustMatchers with EitherValues with O } "fail to bind for invalid values" in { - Json.fromJson[Foo](JsString("invalid")).asEither.left.value must contain(JsPath -> Seq(ValidationError("error.invalid"))) + Json.fromJson[Foo](JsString("invalid")).asEither.left.value must contain(JsPath -> Seq(JsonValidationError("error.invalid"))) } } diff --git a/src/main/g8/test/pages/behaviours/PageBehaviours.scala b/src/main/g8/test/pages/behaviours/PageBehaviours.scala index bc074153..b4b3189c 100644 --- a/src/main/g8/test/pages/behaviours/PageBehaviours.scala +++ b/src/main/g8/test/pages/behaviours/PageBehaviours.scala @@ -1,14 +1,13 @@ package pages.behaviours import generators.Generators +import models.{UserAnswers, UserData} import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.{Arbitrary, Gen} import org.scalatest.prop.PropertyChecks import org.scalatest.{MustMatchers, OptionValues, WordSpec} import pages.QuestionPage import play.api.libs.json._ -import uk.gov.hmrc.http.cache.client.CacheMap -import models.UserAnswers trait PageBehaviours extends WordSpec with MustMatchers with PropertyChecks with Generators with OptionValues { @@ -23,15 +22,15 @@ trait PageBehaviours extends WordSpec with MustMatchers with PropertyChecks with val gen = for { page <- genP - cacheMap <- arbitrary[CacheMap] - } yield (page, cacheMap copy (data = cacheMap.data - page.toString)) + userData <- arbitrary[UserData] + } yield (page, userData copy (data = userData.data - page.toString)) forAll(gen) { - case (page, cacheMap) => + case (page, userData) => - whenever(!cacheMap.data.keySet.contains(page.toString)) { + whenever(!userData.data.keys.contains(page.toString)) { - val userAnswers = UserAnswers(cacheMap) + val userAnswers = UserAnswers(userData) userAnswers.get(page) must be(empty) } } @@ -48,13 +47,13 @@ trait PageBehaviours extends WordSpec with MustMatchers with PropertyChecks with val gen = for { page <- genP savedValue <- arbitrary[A] - cacheMap <- arbitrary[CacheMap] - } yield (page, savedValue, cacheMap copy (data = cacheMap.data + (page.toString -> Json.toJson(savedValue)))) + userData <- arbitrary[UserData] + } yield (page, savedValue, userData copy (data = userData.data + (page.toString -> Json.toJson(savedValue)))) forAll(gen) { - case (page, savedValue, cacheMap) => + case (page, savedValue, userData) => - val userAnswers = UserAnswers(cacheMap) + val userAnswers = UserAnswers(userData) userAnswers.get(page).value mustEqual savedValue } } @@ -71,13 +70,13 @@ trait PageBehaviours extends WordSpec with MustMatchers with PropertyChecks with val gen = for { page <- genP newValue <- arbitrary[A] - cacheMap <- arbitrary[CacheMap] - } yield (page, newValue, cacheMap) + userData <- arbitrary[UserData] + } yield (page, newValue, userData) forAll(gen) { - case (page, newValue, cacheMap) => + case (page, newValue, userData) => - val userAnswers = UserAnswers(cacheMap) + val userAnswers = UserAnswers(userData) val updatedAnswers = userAnswers.set(page, newValue) updatedAnswers.get(page).value mustEqual newValue } @@ -93,13 +92,13 @@ trait PageBehaviours extends WordSpec with MustMatchers with PropertyChecks with val gen = for { page <- genP savedValue <- arbitrary[A] - cacheMap <- arbitrary[CacheMap] - } yield (page, cacheMap copy (data = cacheMap.data + (page.toString -> Json.toJson(savedValue)))) + userData <- arbitrary[UserData] + } yield (page, userData copy (data = userData.data + (page.toString -> Json.toJson(savedValue)))) forAll(gen) { - case (page, cacheMap)=> + case (page, userData) => - val userAnswers = UserAnswers(cacheMap) + val userAnswers = UserAnswers(userData) val updatedAnswers = userAnswers.remove(page) updatedAnswers.get(page) must be(empty) } diff --git a/src/main/g8/test/resources/logback.xml b/src/main/g8/test/resources/logback.xml index 5f257534..1954b9b3 100644 --- a/src/main/g8/test/resources/logback.xml +++ b/src/main/g8/test/resources/logback.xml @@ -4,7 +4,7 @@ %date{ISO8601} level=[%level] logger=[%logger] thread=[%thread] message=[%message] %replace(exception=[%xException]){'^exception=\[\]\$',''}%n - + diff --git a/src/main/g8/test/resources/test.application.conf b/src/main/g8/test/resources/test.application.conf new file mode 100644 index 00000000..2a80a74b --- /dev/null +++ b/src/main/g8/test/resources/test.application.conf @@ -0,0 +1,12 @@ +include "application.conf" + +play.filters.disabled += play.filters.csrf.CSRFFilter + +play.http.secret.key = "some_secret" + +mongo-async-driver { + akka { + log-dead-letters-during-shutdown = off + log-dead-letters = 0 + } +} diff --git a/src/main/g8/test/views/IndexViewSpec.scala b/src/main/g8/test/views/IndexViewSpec.scala index 5abc1656..033e4055 100644 --- a/src/main/g8/test/views/IndexViewSpec.scala +++ b/src/main/g8/test/views/IndexViewSpec.scala @@ -1,14 +1,18 @@ package views import views.behaviours.ViewBehaviours -import views.html.index +import views.html.IndexView class IndexViewSpec extends ViewBehaviours { - def view = () => index(frontendAppConfig)(fakeRequest, messages) - "Index view" must { - behave like normalPage(view, "index", "guidance") + val application = applicationBuilder().build() + + val view = application.injector.instanceOf[IndexView] + + val applyView = view.apply()(fakeRequest, messages) + + behave like normalPage(applyView, "index", "guidance") } } diff --git a/src/main/g8/test/views/SessionExpiredViewSpec.scala b/src/main/g8/test/views/SessionExpiredViewSpec.scala index ac40c00d..9094a792 100644 --- a/src/main/g8/test/views/SessionExpiredViewSpec.scala +++ b/src/main/g8/test/views/SessionExpiredViewSpec.scala @@ -1,14 +1,18 @@ package views import views.behaviours.ViewBehaviours -import views.html.session_expired +import views.html.SessionExpiredView class SessionExpiredViewSpec extends ViewBehaviours { - def view = () => session_expired(frontendAppConfig)(fakeRequest, messages) - "Session Expired view" must { - behave like normalPage(view, "session_expired", "guidance") + val application = applicationBuilder().build() + + val view = application.injector.instanceOf[SessionExpiredView] + + val applyView = view.apply()(fakeRequest, messages) + + behave like normalPage(applyView, "session_expired", "guidance") } } diff --git a/src/main/g8/test/views/UnauthorisedViewSpec.scala b/src/main/g8/test/views/UnauthorisedViewSpec.scala index 658da1ba..7688313f 100644 --- a/src/main/g8/test/views/UnauthorisedViewSpec.scala +++ b/src/main/g8/test/views/UnauthorisedViewSpec.scala @@ -1,14 +1,18 @@ package views import views.behaviours.ViewBehaviours -import views.html.unauthorised +import views.html.UnauthorisedView class UnauthorisedViewSpec extends ViewBehaviours { - def view = () => unauthorised(frontendAppConfig)(fakeRequest, messages) - "Unauthorised view" must { - behave like normalPage(view, "unauthorised") + val application = applicationBuilder().build() + + val view = application.injector.instanceOf[UnauthorisedView] + + val applyView = view.apply()(fakeRequest, messages) + + behave like normalPage(applyView, "unauthorised") } } diff --git a/src/main/g8/test/views/behaviours/IntViewBehaviours.scala b/src/main/g8/test/views/behaviours/IntViewBehaviours.scala index f790c9e7..e050b2ad 100644 --- a/src/main/g8/test/views/behaviours/IntViewBehaviours.scala +++ b/src/main/g8/test/views/behaviours/IntViewBehaviours.scala @@ -1,32 +1,38 @@ package views.behaviours -import play.api.data.{Form, FormError} +import play.api.data.Form import play.twirl.api.HtmlFormat trait IntViewBehaviours extends QuestionViewBehaviours[Int] { - + val number = 123 - def intPage(createView: (Form[Int]) => HtmlFormat.Appendable, + def intPage(form: Form[Int], + createView: Form[Int] => HtmlFormat.Appendable, messageKeyPrefix: String, - expectedFormAction: String) = { + expectedFormAction: String): Unit = { "behave like a page with an integer value field" when { + "rendered" must { "contain a label for the value" in { + val doc = asDocument(createView(form)) assertContainsLabel(doc, "value", messages(s"\$messageKeyPrefix.title")) } "contain an input for the value" in { + val doc = asDocument(createView(form)) assertRenderedById(doc, "value") } } "rendered with a valid form" must { + "include the form's value in the value input" in { + val doc = asDocument(createView(form.fill(number))) doc.getElementById("value").attr("value") mustBe number.toString } @@ -35,17 +41,20 @@ trait IntViewBehaviours extends QuestionViewBehaviours[Int] { "rendered with an error" must { "show an error summary" in { + val doc = asDocument(createView(form.withError(error))) assertRenderedById(doc, "error-summary-heading") } "show an error in the value field's label" in { + val doc = asDocument(createView(form.withError(error))) val errorSpan = doc.getElementsByClass("error-message").first errorSpan.text mustBe messages(errorMessage) } "show an error prefix in the browser title" in { + val doc = asDocument(createView(form.withError(error))) assertEqualsValue(doc, "title", s"""\${messages("error.browser.title.prefix")} \${messages(s"\$messageKeyPrefix.title")}""") } diff --git a/src/main/g8/test/views/behaviours/QuestionViewBehaviours.scala b/src/main/g8/test/views/behaviours/QuestionViewBehaviours.scala index ab80ec9b..8048bcdf 100644 --- a/src/main/g8/test/views/behaviours/QuestionViewBehaviours.scala +++ b/src/main/g8/test/views/behaviours/QuestionViewBehaviours.scala @@ -4,21 +4,25 @@ import play.api.data.{Form, FormError} import play.twirl.api.HtmlFormat trait QuestionViewBehaviours[A] extends ViewBehaviours { - + val errorKey = "value" val errorMessage = "error.number" val error = FormError(errorKey, errorMessage) - + val form: Form[A] - def pageWithTextFields(createView: (Form[A]) => HtmlFormat.Appendable, + def pageWithTextFields(form: Form[A], + createView: Form[A] => HtmlFormat.Appendable, messageKeyPrefix: String, expectedFormAction: String, fields: String*) = { "behave like a question page" when { + "rendered" must { - for(field <- fields) { + + for (field <- fields) { + s"contain an input for \$field" in { val doc = asDocument(createView(form)) assertRenderedById(doc, field) @@ -26,26 +30,33 @@ trait QuestionViewBehaviours[A] extends ViewBehaviours { } "not render an error summary" in { + val doc = asDocument(createView(form)) assertNotRenderedById(doc, "error-summary-heading") } } "rendered with any error" must { + "show an error prefix in the browser title" in { + val doc = asDocument(createView(form.withError(error))) assertEqualsValue(doc, "title", s"""\${messages("error.browser.title.prefix")} \${messages(s"\$messageKeyPrefix.title")}""") } } - for(field <- fields) { + for (field <- fields) { + s"rendered with an error with field '\$field'" must { + "show an error summary" in { + val doc = asDocument(createView(form.withError(FormError(field, "error")))) assertRenderedById(doc, "error-summary-heading") } s"show an error in the label for field '\$field'" in { + val doc = asDocument(createView(form.withError(FormError(field, "error")))) val errorSpan = doc.getElementsByClass("error-message").first errorSpan.parent.attr("for") mustBe field diff --git a/src/main/g8/test/views/behaviours/StringViewBehaviours.scala b/src/main/g8/test/views/behaviours/StringViewBehaviours.scala index e2c725ff..01b69f5a 100644 --- a/src/main/g8/test/views/behaviours/StringViewBehaviours.scala +++ b/src/main/g8/test/views/behaviours/StringViewBehaviours.scala @@ -7,28 +7,34 @@ trait StringViewBehaviours extends QuestionViewBehaviours[String] { val answer = "answer" - def stringPage(createView: (Form[String]) => HtmlFormat.Appendable, + def stringPage(form: Form[String], + createView: Form[String] => HtmlFormat.Appendable, messageKeyPrefix: String, expectedFormAction: String, expectedHintKey: Option[String] = None) = { "behave like a page with a string value field" when { + "rendered" must { "contain a label for the value" in { + val doc = asDocument(createView(form)) - val expectedHintText = expectedHintKey map(k => messages(k)) + val expectedHintText = expectedHintKey map (k => messages(k)) assertContainsLabel(doc, "value", messages(s"\$messageKeyPrefix.heading"), expectedHintText) } "contain an input for the value" in { + val doc = asDocument(createView(form)) assertRenderedById(doc, "value") } } "rendered with a valid form" must { + "include the form's value in the value input" in { + val doc = asDocument(createView(form.fill(answer))) doc.getElementById("value").attr("value") mustBe answer } @@ -37,17 +43,20 @@ trait StringViewBehaviours extends QuestionViewBehaviours[String] { "rendered with an error" must { "show an error summary" in { + val doc = asDocument(createView(form.withError(error))) assertRenderedById(doc, "error-summary-heading") } "show an error in the value field's label" in { + val doc = asDocument(createView(form.withError(error))) val errorSpan = doc.getElementsByClass("error-message").first errorSpan.text mustBe messages(errorMessage) } "show an error prefix in the browser title" in { + val doc = asDocument(createView(form.withError(error))) assertEqualsValue(doc, "title", s"""\${messages("error.browser.title.prefix")} \${messages(s"\$messageKeyPrefix.title")}""") } diff --git a/src/main/g8/test/views/behaviours/ViewBehaviours.scala b/src/main/g8/test/views/behaviours/ViewBehaviours.scala index d0744ad6..04fc1126 100644 --- a/src/main/g8/test/views/behaviours/ViewBehaviours.scala +++ b/src/main/g8/test/views/behaviours/ViewBehaviours.scala @@ -5,47 +5,55 @@ import views.ViewSpecBase trait ViewBehaviours extends ViewSpecBase { - def normalPage(view: () => HtmlFormat.Appendable, + def normalPage(view: HtmlFormat.Appendable, messageKeyPrefix: String, - expectedGuidanceKeys: String*) = { + expectedGuidanceKeys: String*): Unit = { "behave like a normal page" when { + "rendered" must { + "have the correct banner title" in { - val doc = asDocument(view()) + + val doc = asDocument(view) val nav = doc.getElementById("proposition-menu") val span = nav.children.first - span.text mustBe messagesApi("site.service_name") } "display the correct browser title" in { - val doc = asDocument(view()) + + val doc = asDocument(view) assertEqualsMessage(doc, "title", s"\$messageKeyPrefix.title") } "display the correct page title" in { - val doc = asDocument(view()) + + val doc = asDocument(view) assertPageTitleEqualsMessage(doc, s"\$messageKeyPrefix.heading") } "display the correct guidance" in { - val doc = asDocument(view()) + + val doc = asDocument(view) for (key <- expectedGuidanceKeys) assertContainsText(doc, messages(s"\$messageKeyPrefix.\$key")) } "display language toggles" in { - val doc = asDocument(view()) + + val doc = asDocument(view) assertRenderedById(doc, "cymraeg-switch") } } } } - def pageWithBackLink(view: () => HtmlFormat.Appendable) = { - + def pageWithBackLink(view: HtmlFormat.Appendable): Unit = { + "behave like a page with a back link" must { + "have a back link" in { - val doc = asDocument(view()) + + val doc = asDocument(view) assertRenderedById(doc, "back-link") } } diff --git a/src/main/g8/test/views/behaviours/YesNoViewBehaviours.scala b/src/main/g8/test/views/behaviours/YesNoViewBehaviours.scala index cbfa43c2..2d41f215 100644 --- a/src/main/g8/test/views/behaviours/YesNoViewBehaviours.scala +++ b/src/main/g8/test/views/behaviours/YesNoViewBehaviours.scala @@ -1,17 +1,21 @@ package views.behaviours -import play.api.data.{Form, FormError} +import play.api.data.Form import play.twirl.api.HtmlFormat trait YesNoViewBehaviours extends QuestionViewBehaviours[Boolean] { - def yesNoPage(createView: (Form[Boolean]) => HtmlFormat.Appendable, + def yesNoPage(form: Form[Boolean], + createView: Form[Boolean] => HtmlFormat.Appendable, messageKeyPrefix: String, - expectedFormAction: String) = { + expectedFormAction: String): Unit = { "behave like a page with a Yes/No question" when { + "rendered" must { + "contain a legend for the question" in { + val doc = asDocument(createView(form)) val legends = doc.getElementsByTag("legend") legends.size mustBe 1 @@ -19,44 +23,53 @@ trait YesNoViewBehaviours extends QuestionViewBehaviours[Boolean] { } "contain an input for the value" in { + val doc = asDocument(createView(form)) assertRenderedById(doc, "value-yes") assertRenderedById(doc, "value-no") } "have no values checked when rendered with no form" in { + val doc = asDocument(createView(form)) assert(!doc.getElementById("value-yes").hasAttr("checked")) assert(!doc.getElementById("value-no").hasAttr("checked")) } "not render an error summary" in { + val doc = asDocument(createView(form)) assertNotRenderedById(doc, "error-summary_header") } } "rendered with a value of true" must { + behave like answeredYesNoPage(createView, true) } "rendered with a value of false" must { + behave like answeredYesNoPage(createView, false) } "rendered with an error" must { + "show an error summary" in { + val doc = asDocument(createView(form.withError(error))) assertRenderedById(doc, "error-summary-heading") } "show an error in the value field's label" in { + val doc = asDocument(createView(form.withError(error))) val errorSpan = doc.getElementsByClass("error-message").first errorSpan.text mustBe messages(errorMessage) } "show an error prefix in the browser title" in { + val doc = asDocument(createView(form.withError(error))) assertEqualsValue(doc, "title", s"""\${messages("error.browser.title.prefix")} \${messages(s"\$messageKeyPrefix.title")}""") } @@ -65,15 +78,17 @@ trait YesNoViewBehaviours extends QuestionViewBehaviours[Boolean] { } - def answeredYesNoPage(createView: (Form[Boolean]) => HtmlFormat.Appendable, answer: Boolean) = { + def answeredYesNoPage(createView: Form[Boolean] => HtmlFormat.Appendable, answer: Boolean): Unit = { "have only the correct value checked" in { + val doc = asDocument(createView(form.fill(answer))) assert(doc.getElementById("value-yes").hasAttr("checked") == answer) assert(doc.getElementById("value-no").hasAttr("checked") != answer) } "not render an error summary" in { + val doc = asDocument(createView(form.fill(answer))) assertNotRenderedById(doc, "error-summary_header") } diff --git a/src/main/scaffolds/intPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/intPage/app/controllers/$className$Controller.scala index 0510540a..dd96c863 100644 --- a/src/main/scaffolds/intPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/intPage/app/controllers/$className$Controller.scala @@ -1,35 +1,35 @@ package controllers -import javax.inject.Inject - -import play.api.data.Form -import play.api.i18n.{I18nSupport, MessagesApi} -import uk.gov.hmrc.play.bootstrap.controller.FrontendController -import connectors.DataCacheConnector import controllers.actions._ -import config.FrontendAppConfig import forms.$className$FormProvider +import javax.inject.Inject import models.Mode -import pages.$className$Page import navigation.Navigator -import views.html.$className;format="decap"$ +import pages.$className$Page +import play.api.data.Form +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import repositories.SessionRepository +import uk.gov.hmrc.play.bootstrap.controller.FrontendBaseController +import views.html.$className$View -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} class $className$Controller @Inject()( - appConfig: FrontendAppConfig, override val messagesApi: MessagesApi, - dataCacheConnector: DataCacheConnector, + sessionRepository: SessionRepository, navigator: Navigator, identify: IdentifierAction, getData: DataRetrievalAction, requireData: DataRequiredAction, - formProvider: $className$FormProvider - ) extends FrontendController with I18nSupport { + formProvider: $className$FormProvider, + val controllerComponents: MessagesControllerComponents, + view: $className$View + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { val form = formProvider() - def onPageLoad(mode: Mode) = (identify andThen getData andThen requireData) { + def onPageLoad(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData) { implicit request => val preparedForm = request.userAnswers.get($className$Page) match { @@ -37,19 +37,20 @@ class $className$Controller @Inject()( case Some(value) => form.fill(value) } - Ok($className;format="decap"$(appConfig, preparedForm, mode)) + Ok(view(preparedForm, mode)) } - def onSubmit(mode: Mode) = (identify andThen getData andThen requireData).async { + def onSubmit(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData).async { implicit request => form.bindFromRequest().fold( (formWithErrors: Form[_]) => - Future.successful(BadRequest($className;format="decap"$(appConfig, formWithErrors, mode))), - (value) => { + Future.successful(BadRequest(view(formWithErrors, mode))), + + value => { val updatedAnswers = request.userAnswers.set($className$Page, value) - dataCacheConnector.save(updatedAnswers.cacheMap).map( + sessionRepository.set(updatedAnswers.userData).map( _ => Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) ) diff --git a/src/main/scaffolds/intPage/app/forms/$className$FormProvider.scala b/src/main/scaffolds/intPage/app/forms/$className$FormProvider.scala index cbc84934..cc625d4e 100644 --- a/src/main/scaffolds/intPage/app/forms/$className$FormProvider.scala +++ b/src/main/scaffolds/intPage/app/forms/$className$FormProvider.scala @@ -1,8 +1,7 @@ package forms -import javax.inject.Inject - import forms.mappings.Mappings +import javax.inject.Inject import play.api.data.Form class $className$FormProvider @Inject() extends Mappings { diff --git a/src/main/scaffolds/intPage/app/views/$className__decap$.scala.html b/src/main/scaffolds/intPage/app/views/$className$View.scala.html similarity index 53% rename from src/main/scaffolds/intPage/app/views/$className__decap$.scala.html rename to src/main/scaffolds/intPage/app/views/$className$View.scala.html index 19bf0936..c79e7b21 100644 --- a/src/main/scaffolds/intPage/app/views/$className__decap$.scala.html +++ b/src/main/scaffolds/intPage/app/views/$className$View.scala.html @@ -1,17 +1,15 @@ -@import config.FrontendAppConfig -@import uk.gov.hmrc.play.views.html._ -@import controllers.routes._ -@import models.Mode -@import views.ViewUtils._ +@this( + main_template: MainTemplate, + formHelper: FormWithCSRF +) -@(appConfig: FrontendAppConfig, form: Form[_], mode: Mode)(implicit request: Request[_], messages: Messages) +@(form: Form[_], mode: Mode)(implicit request: Request[_], messages: Messages) @main_template( - title = s"\${errorPrefix(form)} \${messages("$className;format="decap"$.title")}", - appConfig = appConfig, - bodyClasses = None) { + title = s"\${errorPrefix(form)} \${messages("$className;format="decap"$.title")}" + ) { - @helpers.form(action = $className$Controller.onSubmit(mode), 'autoComplete -> "off") { + @formHelper(action = $className$Controller.onSubmit(mode), 'autoComplete -> "off") { @components.back_link() diff --git a/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala index 68b6479f..4918120b 100644 --- a/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala @@ -1,83 +1,127 @@ package controllers -import play.api.data.Form -import play.api.libs.json.JsNumber -import uk.gov.hmrc.http.cache.client.CacheMap -import navigation.FakeNavigator -import connectors.FakeDataCacheConnector -import controllers.actions._ -import play.api.test.Helpers._ +import base.SpecBase import forms.$className$FormProvider -import models.NormalMode +import models.{NormalMode, UserData} +import navigation.{FakeNavigator, Navigator} import pages.$className$Page +import play.api.inject.bind +import play.api.libs.json.{JsNumber, Json} import play.api.mvc.Call -import views.html.$className;format="decap"$ - -class $className$ControllerSpec extends ControllerSpecBase { +import play.api.test.FakeRequest +import play.api.test.Helpers._ +import views.html.$className$View - def onwardRoute = Call("GET", "/foo") +class $className$ControllerSpec extends SpecBase { val formProvider = new $className$FormProvider() val form = formProvider() - def controller(dataRetrievalAction: DataRetrievalAction = getEmptyCacheMap) = - new $className$Controller(frontendAppConfig, messagesApi, FakeDataCacheConnector, new FakeNavigator(onwardRoute), FakeIdentifierAction, - dataRetrievalAction, new DataRequiredActionImpl, formProvider) + def onwardRoute = Call("GET", "/foo") - def viewAsString(form: Form[_] = form) = $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages).toString + val validAnswer = $minimum$ - val testNumber = $minimum$ + lazy val $className;format="decap"$Route = routes.$className$Controller.onPageLoad(NormalMode).url "$className$ Controller" must { "return OK and the correct view for a GET" in { - val result = controller().onPageLoad(NormalMode)(fakeRequest) - status(result) mustBe OK - contentAsString(result) mustBe viewAsString() + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val result = route(application, request).value + + val view = application.injector.instanceOf[$className$View] + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view(form, NormalMode)(fakeRequest, messages).toString } "populate the view correctly on a GET when the question has previously been answered" in { - val validData = Map($className$Page.toString -> JsNumber(testNumber)) - val getRelevantData = new FakeDataRetrievalAction(Some(CacheMap(cacheMapId, validData))) - val result = controller(getRelevantData).onPageLoad(NormalMode)(fakeRequest) + val userData = UserData(userDataId, Json.obj($className$Page.toString -> JsNumber(validAnswer))) + + val application = applicationBuilder(userData = Some(userData)).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val view = application.injector.instanceOf[$className$View] + + val result = route(application, request).value + + status(result) mustEqual OK - contentAsString(result) mustBe viewAsString(form.fill(testNumber)) + contentAsString(result) mustEqual + view(form.fill(validAnswer), NormalMode)(fakeRequest, messages).toString } "redirect to the next page when valid data is submitted" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("value", testNumber.toString)) - val result = controller().onSubmit(NormalMode)(postRequest) + val application = + applicationBuilder(userData = Some(emptyUserData)) + .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) + .build() - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(onwardRoute.url) + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", validAnswer.toString)) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual onwardRoute.url } "return a Bad Request and errors when invalid data is submitted" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("value", "invalid value")) + + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", "invalid value")) + val boundForm = form.bind(Map("value" -> "invalid value")) - val result = controller().onSubmit(NormalMode)(postRequest) + val view = application.injector.instanceOf[$className$View] + + val result = route(application, request).value + + status(result) mustEqual BAD_REQUEST - status(result) mustBe BAD_REQUEST - contentAsString(result) mustBe viewAsString(boundForm) + contentAsString(result) mustEqual + view(boundForm, NormalMode)(fakeRequest, messages).toString } "redirect to Session Expired for a GET if no existing data is found" in { - val result = controller(dontGetAnyData).onPageLoad(NormalMode)(fakeRequest) - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(routes.SessionExpiredController.onPageLoad().url) + val application = applicationBuilder(userData = None).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url } "redirect to Session Expired for a POST if no existing data is found" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("value", testNumber.toString)) - val result = controller(dontGetAnyData).onSubmit(NormalMode)(postRequest) - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(routes.SessionExpiredController.onPageLoad().url) + val application = applicationBuilder(userData = None).build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", validAnswer.toString)) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url } } } diff --git a/src/main/scaffolds/intPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/intPage/generated-test/views/$className$ViewSpec.scala index 0340bee4..4d4ed6b9 100644 --- a/src/main/scaffolds/intPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/intPage/generated-test/views/$className$ViewSpec.scala @@ -1,11 +1,12 @@ package views -import play.api.data.Form import controllers.routes import forms.$className$FormProvider import models.NormalMode +import play.api.data.Form +import play.twirl.api.HtmlFormat import views.behaviours.IntViewBehaviours -import views.html.$className;format="decap"$ +import views.html.$className$View class $className$ViewSpec extends IntViewBehaviours { @@ -13,15 +14,19 @@ class $className$ViewSpec extends IntViewBehaviours { val form = new $className$FormProvider()() - def createView = () => $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages) + "$className$View view" must { + + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val view = application.injector.instanceOf[$className$View] - def createViewUsingForm = (form: Form[_]) => $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages) + def applyView(form: Form[_]): HtmlFormat.Appendable = + view.apply(form, NormalMode)(fakeRequest, messages) - "$className$ view" must { - behave like normalPage(createView, messageKeyPrefix) + behave like normalPage(applyView(form), messageKeyPrefix) - behave like pageWithBackLink(createView) + behave like pageWithBackLink(applyView(form)) - behave like intPage(createViewUsingForm, messageKeyPrefix, routes.$className$Controller.onSubmit(NormalMode).url) + behave like intPage(form, applyView, messageKeyPrefix, routes.$className$Controller.onSubmit(NormalMode).url) } } diff --git a/src/main/scaffolds/intPage/migrations/$className__snake$.sh b/src/main/scaffolds/intPage/migrations/$className__snake$.sh index b9754b30..c63d9fe7 100644 --- a/src/main/scaffolds/intPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/intPage/migrations/$className__snake$.sh @@ -1,21 +1,22 @@ #!/bin/bash +echo "" echo "Applying migration $className;format="snake"$" echo "Adding routes to conf/app.routes" echo "" >> ../conf/app.routes -echo "GET /$className;format="decap"$ controllers.$className$Controller.onPageLoad(mode: Mode = NormalMode)" >> ../conf/app.routes -echo "POST /$className;format="decap"$ controllers.$className$Controller.onSubmit(mode: Mode = NormalMode)" >> ../conf/app.routes +echo "GET /$className;format="decap"$ controllers.$className$Controller.onPageLoad(mode: Mode = NormalMode)" >> ../conf/app.routes +echo "POST /$className;format="decap"$ controllers.$className$Controller.onSubmit(mode: Mode = NormalMode)" >> ../conf/app.routes echo "GET /change$className$ controllers.$className$Controller.onPageLoad(mode: Mode = CheckMode)" >> ../conf/app.routes echo "POST /change$className$ controllers.$className$Controller.onSubmit(mode: Mode = CheckMode)" >> ../conf/app.routes echo "Adding messages to conf.messages" echo "" >> ../conf/messages.en -echo "$className;format="decap"$.title = $className;format="decap"$" >> ../conf/messages.en -echo "$className;format="decap"$.heading = $className;format="decap"$" >> ../conf/messages.en -echo "$className;format="decap"$.checkYourAnswersLabel = $className;format="decap"$" >> ../conf/messages.en +echo "$className;format="decap"$.title = $className$" >> ../conf/messages.en +echo "$className;format="decap"$.heading = $className$" >> ../conf/messages.en +echo "$className;format="decap"$.checkYourAnswersLabel = $className$" >> ../conf/messages.en echo "$className;format="decap"$.error.nonNumeric = Enter your $className;format="decap"$ using numbers" >> ../conf/messages.en echo "$className;format="decap"$.error.required = Enter your $className;format="decap"$" >> ../conf/messages.en echo "$className;format="decap"$.error.wholeNumber = Enter your $className;format="decap"$ using whole numbers" >> ../conf/messages.en @@ -42,11 +43,11 @@ awk '/trait PageGenerators/ {\ print " Arbitrary($className$Page)";\ next }1' ../test/generators/PageGenerators.scala > tmp && mv tmp ../test/generators/PageGenerators.scala -echo "Adding to CacheMapGenerator" +echo "Adding to UserDataGenerator" awk '/val generators/ {\ print;\ print " arbitrary[($className$Page.type, JsValue)] ::";\ - next }1' ../test/generators/CacheMapGenerator.scala > tmp && mv tmp ../test/generators/CacheMapGenerator.scala + next }1' ../test/generators/UserDataGenerator.scala > tmp && mv tmp ../test/generators/UserDataGenerator.scala echo "Adding helper method to CheckYourAnswersHelper" awk '/class/ {\ diff --git a/src/main/scaffolds/optionsPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/optionsPage/app/controllers/$className$Controller.scala index cf99a62a..dac73654 100644 --- a/src/main/scaffolds/optionsPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/optionsPage/app/controllers/$className$Controller.scala @@ -1,35 +1,35 @@ package controllers -import javax.inject.Inject - -import play.api.data.Form -import play.api.i18n.{I18nSupport, MessagesApi} -import uk.gov.hmrc.play.bootstrap.controller.FrontendController -import connectors.DataCacheConnector import controllers.actions._ -import config.FrontendAppConfig import forms.$className$FormProvider +import javax.inject.Inject import models.{Enumerable, Mode} -import pages.$className$Page import navigation.Navigator -import views.html.$className;format="decap"$ +import pages.$className$Page +import play.api.data.Form +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import repositories.SessionRepository +import uk.gov.hmrc.play.bootstrap.controller.FrontendBaseController +import views.html.$className$View -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} class $className$Controller @Inject()( - appConfig: FrontendAppConfig, - override val messagesApi: MessagesApi, - dataCacheConnector: DataCacheConnector, - navigator: Navigator, - identify: IdentifierAction, - getData: DataRetrievalAction, - requireData: DataRequiredAction, - formProvider: $className$FormProvider - ) extends FrontendController with I18nSupport with Enumerable.Implicits { + override val messagesApi: MessagesApi, + sessionRepository: SessionRepository, + navigator: Navigator, + identify: IdentifierAction, + getData: DataRetrievalAction, + requireData: DataRequiredAction, + formProvider: $className$FormProvider, + val controllerComponents: MessagesControllerComponents, + view: $className$View + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport with Enumerable.Implicits { val form = formProvider() - def onPageLoad(mode: Mode) = (identify andThen getData andThen requireData) { + def onPageLoad(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData) { implicit request => val preparedForm = request.userAnswers.get($className$Page) match { @@ -37,19 +37,20 @@ class $className$Controller @Inject()( case Some(value) => form.fill(value) } - Ok($className;format="decap"$(appConfig, preparedForm, mode)) + Ok(view(preparedForm, mode)) } - def onSubmit(mode: Mode) = (identify andThen getData andThen requireData).async { + def onSubmit(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData).async { implicit request => form.bindFromRequest().fold( (formWithErrors: Form[_]) => - Future.successful(BadRequest($className;format="decap"$(appConfig, formWithErrors, mode))), - (value) => { + Future.successful(BadRequest(view(formWithErrors, mode))), + + value => { val updatedAnswers = request.userAnswers.set($className$Page, value) - dataCacheConnector.save(updatedAnswers.cacheMap).map( + sessionRepository.set(updatedAnswers.userData).map( _ => Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) ) diff --git a/src/main/scaffolds/optionsPage/app/views/$className__decap$.scala.html b/src/main/scaffolds/optionsPage/app/views/$className$View.scala.html similarity index 57% rename from src/main/scaffolds/optionsPage/app/views/$className__decap$.scala.html rename to src/main/scaffolds/optionsPage/app/views/$className$View.scala.html index ddad8ce6..e01833d1 100644 --- a/src/main/scaffolds/optionsPage/app/views/$className__decap$.scala.html +++ b/src/main/scaffolds/optionsPage/app/views/$className$View.scala.html @@ -1,18 +1,18 @@ -@import config.FrontendAppConfig -@import uk.gov.hmrc.play.views.html._ @import controllers.routes._ -@import models.$className$ -@import models.Mode -@import views.ViewUtils._ +@import models.{Mode, $className$} -@(appConfig: FrontendAppConfig, form: Form[_], mode: Mode)(implicit request: Request[_], messages: Messages) +@this( + main_template: MainTemplate, + formHelper: FormWithCSRF +) + +@(form: Form[_], mode: Mode)(implicit request: Request[_], messages: Messages) @main_template( - title = s"\${errorPrefix(form)} \${messages("$className;format="decap"$.title")}", - appConfig = appConfig, - bodyClasses = None) { + title = s"\${errorPrefix(form)} \${messages("$className;format="decap"$.title")}" + ) { - @helpers.form(action = $className$Controller.onSubmit(mode), 'autoComplete -> "off") { + @formHelper(action = $className$Controller.onSubmit(mode), 'autoComplete -> "off") { @components.back_link() diff --git a/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala index 98517a6f..e01d7534 100644 --- a/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala @@ -1,82 +1,125 @@ package controllers -import play.api.data.Form -import play.api.libs.json.JsString -import uk.gov.hmrc.http.cache.client.CacheMap -import navigation.FakeNavigator -import connectors.FakeDataCacheConnector -import controllers.actions._ -import play.api.test.Helpers._ +import base.SpecBase import forms.$className$FormProvider -import models.NormalMode -import models.$className$ +import models.{NormalMode, $className$, UserData} +import navigation.{FakeNavigator, Navigator} import pages.$className$Page +import play.api.inject.bind +import play.api.libs.json.{JsString, Json} import play.api.mvc.Call -import views.html.$className;format="decap"$ +import play.api.test.FakeRequest +import play.api.test.Helpers._ +import views.html.$className$View -class $className$ControllerSpec extends ControllerSpecBase { +class $className$ControllerSpec extends SpecBase { def onwardRoute = Call("GET", "/foo") + lazy val $className;format="decap"$Route = routes.$className$Controller.onPageLoad(NormalMode).url + val formProvider = new $className$FormProvider() val form = formProvider() - def controller(dataRetrievalAction: DataRetrievalAction = getEmptyCacheMap) = - new $className$Controller(frontendAppConfig, messagesApi, FakeDataCacheConnector, new FakeNavigator(onwardRoute), FakeIdentifierAction, - dataRetrievalAction, new DataRequiredActionImpl, formProvider) - - def viewAsString(form: Form[_] = form) = $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages).toString - "$className$ Controller" must { "return OK and the correct view for a GET" in { - val result = controller().onPageLoad(NormalMode)(fakeRequest) - status(result) mustBe OK - contentAsString(result) mustBe viewAsString() + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val result = route(application, request).value + + val view = application.injector.instanceOf[$className$View] + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view(form, NormalMode)(fakeRequest, messages).toString } "populate the view correctly on a GET when the question has previously been answered" in { - val validData = Map($className$Page.toString -> JsString($className$.values.head.toString)) - val getRelevantData = new FakeDataRetrievalAction(Some(CacheMap(cacheMapId, validData))) - val result = controller(getRelevantData).onPageLoad(NormalMode)(fakeRequest) + val userData = UserData(userDataId, Json.obj($className$Page.toString -> JsString($className$.values.head.toString))) + + val application = applicationBuilder(userData = Some(userData)).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val view = application.injector.instanceOf[$className$View] + + val result = route(application, request).value + + status(result) mustEqual OK - contentAsString(result) mustBe viewAsString(form.fill($className$.values.head)) + contentAsString(result) mustEqual + view(form.fill($className$.values.head), NormalMode)(fakeRequest, messages).toString } "redirect to the next page when valid data is submitted" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("value", $className$.options.head.value)) - val result = controller().onSubmit(NormalMode)(postRequest) + val application = + applicationBuilder(userData = Some(emptyUserData)) + .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) + .build() - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(onwardRoute.url) + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", $className$.options.head.value)) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual onwardRoute.url } "return a Bad Request and errors when invalid data is submitted" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("value", "invalid value")) + + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", "invalid value")) + val boundForm = form.bind(Map("value" -> "invalid value")) - val result = controller().onSubmit(NormalMode)(postRequest) + val view = application.injector.instanceOf[$className$View] + + val result = route(application, request).value - status(result) mustBe BAD_REQUEST - contentAsString(result) mustBe viewAsString(boundForm) + status(result) mustEqual BAD_REQUEST + + contentAsString(result) mustEqual + view(boundForm, NormalMode)(fakeRequest, messages).toString } "redirect to Session Expired for a GET if no existing data is found" in { - val result = controller(dontGetAnyData).onPageLoad(NormalMode)(fakeRequest) - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(routes.SessionExpiredController.onPageLoad().url) + val application = applicationBuilder(userData = None).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url } "redirect to Session Expired for a POST if no existing data is found" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("value", $className$.options.head.value)) - val result = controller(dontGetAnyData).onSubmit(NormalMode)(postRequest) + + val application = applicationBuilder(userData = None).build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", $className$.values.head.toString)) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(routes.SessionExpiredController.onPageLoad().url) + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url } } } diff --git a/src/main/scaffolds/optionsPage/generated-test/pages/$className$PageSpec.scala b/src/main/scaffolds/optionsPage/generated-test/pages/$className$PageSpec.scala index ab833071..acdf7aea 100644 --- a/src/main/scaffolds/optionsPage/generated-test/pages/$className$PageSpec.scala +++ b/src/main/scaffolds/optionsPage/generated-test/pages/$className$PageSpec.scala @@ -5,7 +5,7 @@ import pages.behaviours.PageBehaviours class $className$Spec extends PageBehaviours { - "YourLocation" must { + "$className$Page" must { beRetrievable[$className$]($className$Page) diff --git a/src/main/scaffolds/optionsPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/optionsPage/generated-test/views/$className$ViewSpec.scala index 894b77de..0f4fcf59 100644 --- a/src/main/scaffolds/optionsPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/optionsPage/generated-test/views/$className$ViewSpec.scala @@ -1,11 +1,11 @@ package views -import play.api.data.Form import forms.$className$FormProvider -import models.NormalMode -import models.$className$ +import models.{NormalMode, $className$} +import play.api.data.Form +import play.twirl.api.HtmlFormat import views.behaviours.ViewBehaviours -import views.html.$className;format="decap"$ +import views.html.$className$View class $className$ViewSpec extends ViewBehaviours { @@ -13,33 +13,45 @@ class $className$ViewSpec extends ViewBehaviours { val form = new $className$FormProvider()() - def createView = () => $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages) + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val view = application.injector.instanceOf[$className$View] - def createViewUsingForm = (form: Form[_]) => $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages) + def applyView(form: Form[_]): HtmlFormat.Appendable = + view.apply(form, NormalMode)(fakeRequest, messages) - "$className$ view" must { - behave like normalPage(createView, messageKeyPrefix) + "$className$View" must { - behave like pageWithBackLink(createView) + behave like normalPage(applyView(form), messageKeyPrefix) + + behave like pageWithBackLink(applyView(form)) } - "$className$ view" when { + "$className$View" when { + "rendered" must { + "contain radio buttons for the value" in { - val doc = asDocument(createViewUsingForm(form)) + + val doc = asDocument(applyView(form)) + for (option <- $className$.options) { assertContainsRadioButton(doc, option.id, "value", option.value, false) } } } - for(option <- $className$.options) { + for (option <- $className$.options) { + s"rendered with a value of '\${option.value}'" must { + s"have the '\${option.value}' radio button selected" in { - val doc = asDocument(createViewUsingForm(form.bind(Map("value" -> s"\${option.value}")))) + + val doc = asDocument(applyView(form.bind(Map("value" -> s"\${option.value}")))) + assertContainsRadioButton(doc, option.id, "value", option.value, true) - for(unselectedOption <- $className$.options.filterNot(o => o == option)) { + for (unselectedOption <- $className$.options.filterNot(o => o == option)) { assertContainsRadioButton(doc, unselectedOption.id, "value", unselectedOption.value, false) } } diff --git a/src/main/scaffolds/optionsPage/migrations/$className__snake$.sh b/src/main/scaffolds/optionsPage/migrations/$className__snake$.sh index 120e06f6..99d9b072 100644 --- a/src/main/scaffolds/optionsPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/optionsPage/migrations/$className__snake$.sh @@ -1,12 +1,13 @@ #!/bin/bash +echo "" echo "Applying migration $className;format="snake"$" echo "Adding routes to conf/app.routes" echo "" >> ../conf/app.routes -echo "GET /$className;format="decap"$ controllers.$className$Controller.onPageLoad(mode: Mode = NormalMode)" >> ../conf/app.routes -echo "POST /$className;format="decap"$ controllers.$className$Controller.onSubmit(mode: Mode = NormalMode)" >> ../conf/app.routes +echo "GET /$className;format="decap"$ controllers.$className$Controller.onPageLoad(mode: Mode = NormalMode)" >> ../conf/app.routes +echo "POST /$className;format="decap"$ controllers.$className$Controller.onSubmit(mode: Mode = NormalMode)" >> ../conf/app.routes echo "GET /change$className$ controllers.$className$Controller.onPageLoad(mode: Mode = CheckMode)" >> ../conf/app.routes echo "POST /change$className$ controllers.$className$Controller.onSubmit(mode: Mode = CheckMode)" >> ../conf/app.routes @@ -51,11 +52,11 @@ awk '/trait ModelGenerators/ {\ print " }";\ next }1' ../test/generators/ModelGenerators.scala > tmp && mv tmp ../test/generators/ModelGenerators.scala -echo "Adding to CacheMapGenerator" +echo "Adding to UserDataGenerator" awk '/val generators/ {\ print;\ print " arbitrary[($className$Page.type, JsValue)] ::";\ - next }1' ../test/generators/CacheMapGenerator.scala > tmp && mv tmp ../test/generators/CacheMapGenerator.scala + next }1' ../test/generators/UserDataGenerator.scala > tmp && mv tmp ../test/generators/UserDataGenerator.scala echo "Adding helper method to CheckYourAnswersHelper" awk '/class/ {\ diff --git a/src/main/scaffolds/page/app/controllers/$className$Controller.scala b/src/main/scaffolds/page/app/controllers/$className$Controller.scala index 9cbacb01..f3ab4d9b 100644 --- a/src/main/scaffolds/page/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/page/app/controllers/$className$Controller.scala @@ -1,24 +1,25 @@ package controllers +import controllers.actions._ import javax.inject.Inject - import play.api.i18n.{I18nSupport, MessagesApi} -import uk.gov.hmrc.play.bootstrap.controller.FrontendController -import controllers.actions._ -import config.FrontendAppConfig -import views.html.$className;format="decap"$ +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import uk.gov.hmrc.play.bootstrap.controller.FrontendBaseController +import views.html.$className$View -import scala.concurrent.Future +import scala.concurrent.ExecutionContext -class $className;format="cap"$Controller @Inject()(appConfig: FrontendAppConfig, - override val messagesApi: MessagesApi, - identify: IdentifierAction, - getData: DataRetrievalAction, - requireData: DataRequiredAction - ) extends FrontendController with I18nSupport { +class $className$Controller @Inject()( + override val messagesApi: MessagesApi, + identify: IdentifierAction, + getData: DataRetrievalAction, + requireData: DataRequiredAction, + val controllerComponents: MessagesControllerComponents, + view: $className$View + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { - def onPageLoad = (identify andThen getData andThen requireData) { + def onPageLoad: Action[AnyContent] = (identify andThen getData andThen requireData) { implicit request => - Ok($className;format="decap"$(appConfig)) + Ok(view()) } } diff --git a/src/main/scaffolds/page/app/views/$className$View.scala.html b/src/main/scaffolds/page/app/views/$className$View.scala.html new file mode 100644 index 00000000..4652e1b9 --- /dev/null +++ b/src/main/scaffolds/page/app/views/$className$View.scala.html @@ -0,0 +1,14 @@ +@this( + main_template: MainTemplate +) + +@()(implicit request: Request[_], messages: Messages) + +@main_template( + title = messages("$className;format="decap"$.title") + ) { + + @components.back_link() + + @components.heading("$className;format="decap"$.heading") +} diff --git a/src/main/scaffolds/page/app/views/$className__decap$.scala.html b/src/main/scaffolds/page/app/views/$className__decap$.scala.html deleted file mode 100644 index 5e290b78..00000000 --- a/src/main/scaffolds/page/app/views/$className__decap$.scala.html +++ /dev/null @@ -1,13 +0,0 @@ -@import config.FrontendAppConfig - -@(appConfig: FrontendAppConfig)(implicit request: Request[_], messages: Messages) - -@main_template( - title = messages("$className;format="decap"$.title"), - appConfig = appConfig, - bodyClasses = None) { - - @components.back_link() - - @components.heading("$className;format="decap"$.heading") -} diff --git a/src/main/scaffolds/page/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/page/generated-test/controllers/$className$ControllerSpec.scala index 8cc98c83..a8938437 100644 --- a/src/main/scaffolds/page/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/page/generated-test/controllers/$className$ControllerSpec.scala @@ -1,28 +1,28 @@ package controllers -import controllers.actions._ +import base.SpecBase +import play.api.test.FakeRequest import play.api.test.Helpers._ -import views.html.$className;format="decap"$ +import views.html.$className$View -class $className$ControllerSpec extends ControllerSpecBase { - - def controller(dataRetrievalAction: DataRetrievalAction = getEmptyCacheMap) = - new $className$Controller(frontendAppConfig, messagesApi, FakeIdentifierAction, - dataRetrievalAction, new DataRequiredActionImpl) - - def viewAsString() = $className;format="decap"$(frontendAppConfig)(fakeRequest, messages).toString +class $className$ControllerSpec extends SpecBase { "$className$ Controller" must { "return OK and the correct view for a GET" in { - val result = controller().onPageLoad(fakeRequest) - status(result) mustBe OK - contentAsString(result) mustBe viewAsString() - } - } -} + val application = applicationBuilder(userData = Some(emptyUserData)).build() + val request = FakeRequest(GET, routes.$className$Controller.onPageLoad().url) + val result = route(application, request).value + val view = application.injector.instanceOf[$className$View] + status(result) mustEqual OK + + contentAsString(result) mustEqual + view()(fakeRequest, messages).toString + } + } +} diff --git a/src/main/scaffolds/page/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/page/generated-test/views/$className$ViewSpec.scala index 59bed332..6cc8f53e 100644 --- a/src/main/scaffolds/page/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/page/generated-test/views/$className$ViewSpec.scala @@ -1,17 +1,20 @@ package views import views.behaviours.ViewBehaviours -import views.html.$className;format="decap"$ +import views.html.$className$View class $className$ViewSpec extends ViewBehaviours { - val messageKeyPrefix = "$className;format="decap"$" + "$className$ view" must { - def createView = () => $className;format="decap"$(frontendAppConfig)(fakeRequest, messages) + val application = applicationBuilder(userData = Some(emptyUserData)).build() - "$className$ view" must { - behave like normalPage(createView, messageKeyPrefix) + val view = application.injector.instanceOf[$className$View] + + val applyView = view.apply()(fakeRequest, messages) + + behave like normalPage(applyView, "$className;format="decap"$") - behave like pageWithBackLink(createView) + behave like pageWithBackLink(applyView) } } diff --git a/src/main/scaffolds/page/migrations/$className__snake$.sh b/src/main/scaffolds/page/migrations/$className__snake$.sh index 5293aa1e..4003dc21 100644 --- a/src/main/scaffolds/page/migrations/$className__snake$.sh +++ b/src/main/scaffolds/page/migrations/$className__snake$.sh @@ -1,5 +1,6 @@ #!/bin/bash +echo "" echo "Applying migration $className;format="snake"$" echo "Adding routes to conf/app.routes" diff --git a/src/main/scaffolds/questionPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/questionPage/app/controllers/$className$Controller.scala index db68f073..4897ae12 100644 --- a/src/main/scaffolds/questionPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/questionPage/app/controllers/$className$Controller.scala @@ -1,34 +1,35 @@ package controllers -import javax.inject.Inject - -import play.api.data.Form -import play.api.i18n.{I18nSupport, MessagesApi} -import uk.gov.hmrc.play.bootstrap.controller.FrontendController -import connectors.DataCacheConnector import controllers.actions._ -import config.FrontendAppConfig import forms.$className$FormProvider +import javax.inject.Inject import models.Mode -import pages.$className$Page import navigation.Navigator -import views.html.$className;format="decap"$ +import pages.$className$Page +import play.api.data.Form +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import repositories.SessionRepository +import uk.gov.hmrc.play.bootstrap.controller.FrontendBaseController +import views.html.$className$View -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} -class $className$Controller @Inject()(appConfig: FrontendAppConfig, +class $className$Controller @Inject()( override val messagesApi: MessagesApi, - dataCacheConnector: DataCacheConnector, + sessionRepository: SessionRepository, navigator: Navigator, identify: IdentifierAction, getData: DataRetrievalAction, requireData: DataRequiredAction, - formProvider: $className$FormProvider - ) extends FrontendController with I18nSupport { + formProvider: $className$FormProvider, + val controllerComponents: MessagesControllerComponents, + view: $className$View + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { val form = formProvider() - def onPageLoad(mode: Mode) = (identify andThen getData andThen requireData) { + def onPageLoad(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData) { implicit request => val preparedForm = request.userAnswers.get($className$Page) match { @@ -36,19 +37,20 @@ class $className$Controller @Inject()(appConfig: FrontendAppConfig, case Some(value) => form.fill(value) } - Ok($className;format="decap"$(appConfig, preparedForm, mode)) + Ok(view(preparedForm, mode)) } - def onSubmit(mode: Mode) = (identify andThen getData andThen requireData).async { + def onSubmit(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData).async { implicit request => form.bindFromRequest().fold( (formWithErrors: Form[_]) => - Future.successful(BadRequest($className;format="decap"$(appConfig, formWithErrors, mode))), - (value) => { + Future.successful(BadRequest(view(formWithErrors, mode))), + + value => { val updatedAnswers = request.userAnswers.set($className$Page, value) - dataCacheConnector.save(updatedAnswers.cacheMap).map( + sessionRepository.set(updatedAnswers.userData).map( _ => Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) ) diff --git a/src/main/scaffolds/questionPage/app/views/$className__decap$.scala.html b/src/main/scaffolds/questionPage/app/views/$className$View.scala.html similarity index 58% rename from src/main/scaffolds/questionPage/app/views/$className__decap$.scala.html rename to src/main/scaffolds/questionPage/app/views/$className$View.scala.html index 12bc5f4f..5bbf57c7 100644 --- a/src/main/scaffolds/questionPage/app/views/$className__decap$.scala.html +++ b/src/main/scaffolds/questionPage/app/views/$className$View.scala.html @@ -1,17 +1,18 @@ -@import config.FrontendAppConfig -@import uk.gov.hmrc.play.views.html._ @import controllers.routes._ -@import models.{Mode, $className$} -@import views.ViewUtils._ +@import models.Mode -@(appConfig: FrontendAppConfig, form: Form[_], mode: Mode)(implicit request: Request[_], messages: Messages) +@this( + main_template: MainTemplate, + formHelper: FormWithCSRF +) + +@(form: Form[_], mode: Mode)(implicit request: Request[_], messages: Messages) @main_template( - title = s"\${errorPrefix(form)} \${messages("$className;format="decap"$.title")}", - appConfig = appConfig, - bodyClasses = None) { + title = s"\${errorPrefix(form)} \${messages("$className;format="decap"$.title")}" + ) { - @helpers.form(action = $className;format="cap"$Controller.onSubmit(mode), 'autoComplete -> "off") { + @formHelper(action = $className;format="cap"$Controller.onSubmit(mode), 'autoComplete -> "off") { @components.back_link() diff --git a/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala index 05a8f98a..90877c51 100644 --- a/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala @@ -1,81 +1,133 @@ package controllers -import play.api.data.Form -import play.api.libs.json.Json -import uk.gov.hmrc.http.cache.client.CacheMap -import navigation.FakeNavigator -import connectors.FakeDataCacheConnector -import controllers.actions._ -import play.api.test.Helpers._ +import base.SpecBase import forms.$className$FormProvider -import models.{NormalMode, $className$} +import models.{NormalMode, $className$, UserData} +import navigation.{FakeNavigator, Navigator} import pages.$className$Page +import play.api.inject.bind +import play.api.libs.json.Json import play.api.mvc.Call -import views.html.$className;format="decap"$ +import play.api.test.FakeRequest +import play.api.test.Helpers._ +import views.html.$className$View -class $className$ControllerSpec extends ControllerSpecBase { +class $className$ControllerSpec extends SpecBase { def onwardRoute = Call("GET", "/foo") val formProvider = new $className$FormProvider() val form = formProvider() - def controller(dataRetrievalAction: DataRetrievalAction = getEmptyCacheMap) = - new $className$Controller(frontendAppConfig, messagesApi, FakeDataCacheConnector, new FakeNavigator(onwardRoute), FakeIdentifierAction, - dataRetrievalAction, new DataRequiredActionImpl, formProvider) + lazy val $className;format="decap"$Route = routes.$className$Controller.onPageLoad(NormalMode).url - def viewAsString(form: Form[_] = form) = $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages).toString + val userData = UserData( + userDataId, + Json.obj( + $className$Page.toString -> Json.obj( + "field1" -> "value 1", + "field2" -> "value 2" + ) + ) + ) "$className$ Controller" must { "return OK and the correct view for a GET" in { - val result = controller().onPageLoad(NormalMode)(fakeRequest) - status(result) mustBe OK - contentAsString(result) mustBe viewAsString() + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val view = application.injector.instanceOf[$className$View] + + val result = route(application, request).value + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view(form, NormalMode)(request, messages).toString } "populate the view correctly on a GET when the question has previously been answered" in { - val validData = Map($className$Page.toString -> Json.toJson($className$("value 1", "value 2"))) - val getRelevantData = new FakeDataRetrievalAction(Some(CacheMap(cacheMapId, validData))) - val result = controller(getRelevantData).onPageLoad(NormalMode)(fakeRequest) + val application = applicationBuilder(userData = Some(userData)).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val view = application.injector.instanceOf[$className$View] - contentAsString(result) mustBe viewAsString(form.fill($className$("value 1", "value 2"))) + val result = route(application, request).value + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view(form.fill($className$("value 1", "value 2")), NormalMode)(fakeRequest, messages).toString } "redirect to the next page when valid data is submitted" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("field1", "value 1"), ("field2", "value 2")) - val result = controller().onSubmit(NormalMode)(postRequest) + val application = + applicationBuilder(userData = Some(emptyUserData)) + .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) + .build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("field1", "value 1"), ("field2", "value 2")) - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(onwardRoute.url) + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual onwardRoute.url } "return a Bad Request and errors when invalid data is submitted" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("value", "invalid value")) + + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", "invalid value")) + val boundForm = form.bind(Map("value" -> "invalid value")) - val result = controller().onSubmit(NormalMode)(postRequest) + val view = application.injector.instanceOf[$className$View] + + val result = route(application, request).value + + status(result) mustEqual BAD_REQUEST - status(result) mustBe BAD_REQUEST - contentAsString(result) mustBe viewAsString(boundForm) + contentAsString(result) mustEqual + view(boundForm, NormalMode)(fakeRequest, messages).toString } "redirect to Session Expired for a GET if no existing data is found" in { - val result = controller(dontGetAnyData).onPageLoad(NormalMode)(fakeRequest) - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(routes.SessionExpiredController.onPageLoad().url) + val application = applicationBuilder(userData = None).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url } "redirect to Session Expired for a POST if no existing data is found" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("field1", "value 1"), ("field2", "value 2")) - val result = controller(dontGetAnyData).onSubmit(NormalMode)(postRequest) - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(routes.SessionExpiredController.onPageLoad().url) + val application = applicationBuilder(userData = None).build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("field1", "value 1"), ("field2", "value 2")) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url } } } diff --git a/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala index c24410a7..8f25f26d 100644 --- a/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala @@ -1,11 +1,12 @@ package views -import play.api.data.Form import controllers.routes import forms.$className$FormProvider import models.{NormalMode, $className$} +import play.api.data.Form +import play.twirl.api.HtmlFormat import views.behaviours.QuestionViewBehaviours -import views.html.$className;format="decap"$ +import views.html.$className$View class $className$ViewSpec extends QuestionViewBehaviours[$className$] { @@ -13,19 +14,23 @@ class $className$ViewSpec extends QuestionViewBehaviours[$className$] { override val form = new $className$FormProvider()() - def createView = () => $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages) + "$className$View" must { + + val application = applicationBuilder(userData = Some(emptyUserData)).build() - def createViewUsingForm = (form: Form[_]) => $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages) + val view = application.injector.instanceOf[$className$View] + def applyView(form: Form[_]): HtmlFormat.Appendable = + view.apply(form, NormalMode)(fakeRequest, messages) - "$className$ view" must { - behave like normalPage(createView, messageKeyPrefix) + behave like normalPage(applyView(form), messageKeyPrefix) - behave like pageWithBackLink(createView) + behave like pageWithBackLink(applyView(form)) behave like pageWithTextFields( - createViewUsingForm, + form, + applyView, messageKeyPrefix, routes.$className$Controller.onSubmit(NormalMode).url, "field1", "field2" diff --git a/src/main/scaffolds/questionPage/migrations/$className__snake$.sh b/src/main/scaffolds/questionPage/migrations/$className__snake$.sh index 4d18501e..e5a92b72 100644 --- a/src/main/scaffolds/questionPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/questionPage/migrations/$className__snake$.sh @@ -1,5 +1,6 @@ #!/bin/bash +echo "" echo "Applying migration $className;format="snake"$" echo "Adding routes to conf/app.routes" @@ -57,11 +58,11 @@ awk '/trait ModelGenerators/ {\ print " }";\ next }1' ../test/generators/ModelGenerators.scala > tmp && mv tmp ../test/generators/ModelGenerators.scala -echo "Adding to CacheMapGenerator" +echo "Adding to UserDataGenerator" awk '/val generators/ {\ print;\ print " arbitrary[($className$Page.type, JsValue)] ::";\ - next }1' ../test/generators/CacheMapGenerator.scala > tmp && mv tmp ../test/generators/CacheMapGenerator.scala + next }1' ../test/generators/UserDataGenerator.scala > tmp && mv tmp ../test/generators/UserDataGenerator.scala echo "Adding helper method to CheckYourAnswersHelper" awk '/class/ {\ @@ -72,4 +73,4 @@ awk '/class/ {\ print " }";\ next }1' ../app/utils/CheckYourAnswersHelper.scala > tmp && mv tmp ../app/utils/CheckYourAnswersHelper.scala -echo "Migration $className;format="snake"$ completed" \ No newline at end of file +echo "Migration $className;format="snake"$ completed" diff --git a/src/main/scaffolds/stringPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/stringPage/app/controllers/$className$Controller.scala index 0510540a..f70aacb6 100644 --- a/src/main/scaffolds/stringPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/stringPage/app/controllers/$className$Controller.scala @@ -1,35 +1,35 @@ package controllers -import javax.inject.Inject - -import play.api.data.Form -import play.api.i18n.{I18nSupport, MessagesApi} -import uk.gov.hmrc.play.bootstrap.controller.FrontendController -import connectors.DataCacheConnector import controllers.actions._ -import config.FrontendAppConfig import forms.$className$FormProvider +import javax.inject.Inject import models.Mode -import pages.$className$Page import navigation.Navigator -import views.html.$className;format="decap"$ +import pages.$className$Page +import play.api.data.Form +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import repositories.SessionRepository +import uk.gov.hmrc.play.bootstrap.controller.FrontendBaseController +import views.html.$className$View -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} class $className$Controller @Inject()( - appConfig: FrontendAppConfig, override val messagesApi: MessagesApi, - dataCacheConnector: DataCacheConnector, + sessionRepository: SessionRepository, navigator: Navigator, identify: IdentifierAction, getData: DataRetrievalAction, requireData: DataRequiredAction, - formProvider: $className$FormProvider - ) extends FrontendController with I18nSupport { + formProvider: $className$FormProvider, + val controllerComponents: MessagesControllerComponents, + view: $className$View + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { val form = formProvider() - def onPageLoad(mode: Mode) = (identify andThen getData andThen requireData) { + def onPageLoad(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData) { implicit request => val preparedForm = request.userAnswers.get($className$Page) match { @@ -37,19 +37,20 @@ class $className$Controller @Inject()( case Some(value) => form.fill(value) } - Ok($className;format="decap"$(appConfig, preparedForm, mode)) + Ok(view(preparedForm, mode)) } - def onSubmit(mode: Mode) = (identify andThen getData andThen requireData).async { + def onSubmit(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData).async { implicit request => form.bindFromRequest().fold( (formWithErrors: Form[_]) => - Future.successful(BadRequest($className;format="decap"$(appConfig, formWithErrors, mode))), - (value) => { + Future.successful(BadRequest(view(formWithErrors, mode))), + + value => { val updatedAnswers = request.userAnswers.set($className$Page, value) - dataCacheConnector.save(updatedAnswers.cacheMap).map( + sessionRepository.set(updatedAnswers.userData).map( _ => Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) ) diff --git a/src/main/scaffolds/stringPage/app/views/$className__decap$.scala.html b/src/main/scaffolds/stringPage/app/views/$className$View.scala.html similarity index 57% rename from src/main/scaffolds/stringPage/app/views/$className__decap$.scala.html rename to src/main/scaffolds/stringPage/app/views/$className$View.scala.html index b65e46fc..ab876d11 100644 --- a/src/main/scaffolds/stringPage/app/views/$className__decap$.scala.html +++ b/src/main/scaffolds/stringPage/app/views/$className$View.scala.html @@ -1,17 +1,18 @@ -@import config.FrontendAppConfig -@import uk.gov.hmrc.play.views.html._ @import controllers.routes._ @import models.Mode -@import views.ViewUtils._ -@(appConfig: FrontendAppConfig, form: Form[_], mode: Mode)(implicit request: Request[_], messages: Messages) +@this( + main_template: MainTemplate, + formHelper: FormWithCSRF +) + +@(form: Form[_], mode: Mode)(implicit request: Request[_], messages: Messages) @main_template( - title = s"\${errorPrefix(form)} \${messages("$className;format="decap"$.title")}", - appConfig = appConfig, - bodyClasses = None) { + title = s"\${errorPrefix(form)} \${messages("$className;format="decap"$.title")}" + ) { - @helpers.form(action = $className$Controller.onSubmit(mode), 'autoComplete -> "off") { + @formHelper(action = $className$Controller.onSubmit(mode), 'autoComplete -> "off") { @components.back_link() diff --git a/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala index 13fa459b..9e89311a 100644 --- a/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala @@ -1,83 +1,125 @@ package controllers -import play.api.data.Form -import play.api.libs.json.JsString -import uk.gov.hmrc.http.cache.client.CacheMap -import navigation.FakeNavigator -import connectors.FakeDataCacheConnector -import controllers.actions._ -import play.api.test.Helpers._ +import base.SpecBase import forms.$className$FormProvider -import models.NormalMode +import models.{NormalMode, UserData} +import navigation.{FakeNavigator, Navigator} import pages.$className$Page +import play.api.inject.bind +import play.api.libs.json.{JsString, Json} import play.api.mvc.Call -import views.html.$className;format="decap"$ +import play.api.test.FakeRequest +import play.api.test.Helpers._ +import views.html.$className$View -class $className$ControllerSpec extends ControllerSpecBase { +class $className$ControllerSpec extends SpecBase { def onwardRoute = Call("GET", "/foo") val formProvider = new $className$FormProvider() val form = formProvider() - def controller(dataRetrievalAction: DataRetrievalAction = getEmptyCacheMap) = - new $className$Controller(frontendAppConfig, messagesApi, FakeDataCacheConnector, new FakeNavigator(onwardRoute), FakeIdentifierAction, - dataRetrievalAction, new DataRequiredActionImpl, formProvider) - - def viewAsString(form: Form[_] = form) = $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages).toString - - val testAnswer = "answer" + lazy val $className;format="decap"$Route = routes.$className$Controller.onPageLoad(NormalMode).url "$className$ Controller" must { "return OK and the correct view for a GET" in { - val result = controller().onPageLoad(NormalMode)(fakeRequest) - status(result) mustBe OK - contentAsString(result) mustBe viewAsString() + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val result = route(application, request).value + + val view = application.injector.instanceOf[$className$View] + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view(form, NormalMode)(fakeRequest, messages).toString } "populate the view correctly on a GET when the question has previously been answered" in { - val validData = Map($className$Page.toString -> JsString(testAnswer)) - val getRelevantData = new FakeDataRetrievalAction(Some(CacheMap(cacheMapId, validData))) - val result = controller(getRelevantData).onPageLoad(NormalMode)(fakeRequest) + val userData = UserData(userDataId, Json.obj($className$Page.toString -> JsString("answer"))) + + val application = applicationBuilder(userData = Some(userData)).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val view = application.injector.instanceOf[$className$View] - contentAsString(result) mustBe viewAsString(form.fill(testAnswer)) + val result = route(application, request).value + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view(form.fill("answer"), NormalMode)(fakeRequest, messages).toString } "redirect to the next page when valid data is submitted" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("value", testAnswer)) - val result = controller().onSubmit(NormalMode)(postRequest) + val application = + applicationBuilder(userData = Some(emptyUserData)) + .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) + .build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", "answer")) - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(onwardRoute.url) + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual onwardRoute.url } "return a Bad Request and errors when invalid data is submitted" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("value", "")) + + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", "")) + val boundForm = form.bind(Map("value" -> "")) - val result = controller().onSubmit(NormalMode)(postRequest) + val view = application.injector.instanceOf[$className$View] - status(result) mustBe BAD_REQUEST - contentAsString(result) mustBe viewAsString(boundForm) + val result = route(application, request).value + + status(result) mustEqual BAD_REQUEST + + contentAsString(result) mustEqual + view(boundForm, NormalMode)(fakeRequest, messages).toString } "redirect to Session Expired for a GET if no existing data is found" in { - val result = controller(dontGetAnyData).onPageLoad(NormalMode)(fakeRequest) - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(routes.SessionExpiredController.onPageLoad().url) + val application = applicationBuilder(userData = None).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url } "redirect to Session Expired for a POST if no existing data is found" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("value", testAnswer)) - val result = controller(dontGetAnyData).onSubmit(NormalMode)(postRequest) - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(routes.SessionExpiredController.onPageLoad().url) + val application = applicationBuilder(userData = None).build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", "answer")) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url } } } diff --git a/src/main/scaffolds/stringPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/stringPage/generated-test/views/$className$ViewSpec.scala index 3bd63865..e7098da8 100644 --- a/src/main/scaffolds/stringPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/stringPage/generated-test/views/$className$ViewSpec.scala @@ -1,11 +1,12 @@ package views -import play.api.data.Form import controllers.routes import forms.$className$FormProvider import models.NormalMode +import play.api.data.Form +import play.twirl.api.HtmlFormat import views.behaviours.StringViewBehaviours -import views.html.$className;format="decap"$ +import views.html.$className$View class $className$ViewSpec extends StringViewBehaviours { @@ -13,15 +14,19 @@ class $className$ViewSpec extends StringViewBehaviours { val form = new $className$FormProvider()() - def createView = () => $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages) + "$className$View view" must { + + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val view = application.injector.instanceOf[$className$View] - def createViewUsingForm = (form: Form[String]) => $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages) + def applyView(form: Form[_]): HtmlFormat.Appendable = + view.apply(form, NormalMode)(fakeRequest, messages) - "$className$ view" must { - behave like normalPage(createView, messageKeyPrefix) + behave like normalPage(applyView(form), messageKeyPrefix) - behave like pageWithBackLink(createView) + behave like pageWithBackLink(applyView(form)) - behave like stringPage(createViewUsingForm, messageKeyPrefix, routes.$className$Controller.onSubmit(NormalMode).url) + behave like stringPage(form, applyView, messageKeyPrefix, routes.$className$Controller.onSubmit(NormalMode).url) } } diff --git a/src/main/scaffolds/stringPage/migrations/$className__snake$.sh b/src/main/scaffolds/stringPage/migrations/$className__snake$.sh index 5cc1a398..b71e938f 100644 --- a/src/main/scaffolds/stringPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/stringPage/migrations/$className__snake$.sh @@ -1,5 +1,6 @@ #!/bin/bash +echo "" echo "Applying migration $className;format="snake"$" echo "Adding routes to conf/app.routes" @@ -40,11 +41,11 @@ awk '/trait PageGenerators/ {\ print " Arbitrary($className$Page)";\ next }1' ../test/generators/PageGenerators.scala > tmp && mv tmp ../test/generators/PageGenerators.scala -echo "Adding to CacheMapGenerator" +echo "Adding to UserDataGenerator" awk '/val generators/ {\ print;\ print " arbitrary[($className$Page.type, JsValue)] ::";\ - next }1' ../test/generators/CacheMapGenerator.scala > tmp && mv tmp ../test/generators/CacheMapGenerator.scala + next }1' ../test/generators/UserDataGenerator.scala > tmp && mv tmp ../test/generators/UserDataGenerator.scala echo "Adding helper method to CheckYourAnswersHelper" awk '/class/ {\ diff --git a/src/main/scaffolds/yesNoPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/yesNoPage/app/controllers/$className$Controller.scala index 2c999314..5ef1695f 100644 --- a/src/main/scaffolds/yesNoPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/yesNoPage/app/controllers/$className$Controller.scala @@ -1,34 +1,35 @@ package controllers -import javax.inject.Inject - -import play.api.data.Form -import play.api.i18n.{I18nSupport, MessagesApi} -import uk.gov.hmrc.play.bootstrap.controller.FrontendController -import connectors.DataCacheConnector import controllers.actions._ -import config.FrontendAppConfig import forms.$className$FormProvider -import models.Mode -import pages.$className$Page +import javax.inject.Inject +import models.{Mode, UserAnswers} import navigation.Navigator -import views.html.$className;format="decap"$ +import pages.$className$Page +import play.api.data.Form +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import repositories.SessionRepository +import uk.gov.hmrc.play.bootstrap.controller.FrontendBaseController +import views.html.$className$View -import scala.concurrent.Future +import scala.concurrent.{ExecutionContext, Future} -class $className;format="cap"$Controller @Inject()(appConfig: FrontendAppConfig, +class $className;format="cap"$Controller @Inject()( override val messagesApi: MessagesApi, - dataCacheConnector: DataCacheConnector, + sessionRepository: SessionRepository, navigator: Navigator, identify: IdentifierAction, getData: DataRetrievalAction, requireData: DataRequiredAction, - formProvider: $className$FormProvider - ) extends FrontendController with I18nSupport { + formProvider: $className$FormProvider, + val controllerComponents: MessagesControllerComponents, + view: $className$View + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { val form: Form[Boolean] = formProvider() - def onPageLoad(mode: Mode) = (identify andThen getData andThen requireData) { + def onPageLoad(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData) { implicit request => val preparedForm = request.userAnswers.get($className$Page) match { @@ -36,7 +37,7 @@ class $className;format="cap"$Controller @Inject()(appConfig: FrontendAppConfig, case Some(value) => form.fill(value) } - Ok($className;format="decap"$(appConfig, preparedForm, mode)) + Ok(view(preparedForm, mode)) } def onSubmit(mode: Mode) = (identify andThen getData andThen requireData).async { @@ -44,11 +45,12 @@ class $className;format="cap"$Controller @Inject()(appConfig: FrontendAppConfig, form.bindFromRequest().fold( (formWithErrors: Form[_]) => - Future.successful(BadRequest($className;format="decap"$(appConfig, formWithErrors, mode))), - (value) => { + Future.successful(BadRequest(view(formWithErrors, mode))), + + value => { val updatedAnswers = request.userAnswers.set($className$Page, value) - dataCacheConnector.save(updatedAnswers.cacheMap).map( + sessionRepository.set(updatedAnswers.userData).map( _ => Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) ) diff --git a/src/main/scaffolds/yesNoPage/app/views/$className__decap$.scala.html b/src/main/scaffolds/yesNoPage/app/views/$className$View.scala.html similarity index 58% rename from src/main/scaffolds/yesNoPage/app/views/$className__decap$.scala.html rename to src/main/scaffolds/yesNoPage/app/views/$className$View.scala.html index 92b8564c..814a4d7f 100644 --- a/src/main/scaffolds/yesNoPage/app/views/$className__decap$.scala.html +++ b/src/main/scaffolds/yesNoPage/app/views/$className$View.scala.html @@ -1,17 +1,18 @@ -@import config.FrontendAppConfig -@import uk.gov.hmrc.play.views.html._ @import controllers.routes._ @import models.Mode -@import views.ViewUtils._ -@(appConfig: FrontendAppConfig, form: Form[_], mode: Mode)(implicit request: Request[_], messages: Messages) +@this( + main_template: MainTemplate, + formHelper: FormWithCSRF +) + +@(form: Form[_], mode: Mode)(implicit request: Request[_], messages: Messages) @main_template( - title = s"\${errorPrefix(form)} \${messages("$className;format="decap"$.title")}", - appConfig = appConfig, - bodyClasses = None) { + title = s"\${errorPrefix(form)} \${messages("$className;format="decap"$.title")}" + ) { - @helpers.form(action = $className$Controller.onSubmit(mode), 'autoComplete -> "off") { + @formHelper(action = $className$Controller.onSubmit(mode), 'autoComplete -> "off") { @components.back_link() diff --git a/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala index 06086ccf..9edbcec6 100644 --- a/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala @@ -1,81 +1,126 @@ package controllers -import play.api.data.Form -import play.api.libs.json.JsBoolean -import uk.gov.hmrc.http.cache.client.CacheMap -import navigation.FakeNavigator -import connectors.FakeDataCacheConnector -import controllers.actions._ -import play.api.test.Helpers._ +import base.SpecBase import forms.$className$FormProvider -import models.NormalMode +import models.{NormalMode, UserData} +import navigation.{FakeNavigator, Navigator} import pages.$className$Page +import play.api.inject.bind +import play.api.libs.json.{JsBoolean, Json} import play.api.mvc.Call -import views.html.$className;format="decap"$ +import play.api.test.FakeRequest +import play.api.test.Helpers._ +import views.html.$className$View -class $className$ControllerSpec extends ControllerSpecBase { +class $className$ControllerSpec extends SpecBase { def onwardRoute = Call("GET", "/foo") val formProvider = new $className$FormProvider() val form = formProvider() - def controller(dataRetrievalAction: DataRetrievalAction = getEmptyCacheMap) = - new $className$Controller(frontendAppConfig, messagesApi, FakeDataCacheConnector, new FakeNavigator(onwardRoute), FakeIdentifierAction, - dataRetrievalAction, new DataRequiredActionImpl, formProvider) - - def viewAsString(form: Form[_] = form) = $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages).toString + lazy val $className;format="decap"$Route = routes.$className$Controller.onPageLoad(NormalMode).url "$className$ Controller" must { "return OK and the correct view for a GET" in { - val result = controller().onPageLoad(NormalMode)(fakeRequest) - status(result) mustBe OK - contentAsString(result) mustBe viewAsString() + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val result = route(application, request).value + + val view = application.injector.instanceOf[$className$View] + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view(form, NormalMode)(fakeRequest, messages).toString } "populate the view correctly on a GET when the question has previously been answered" in { - val validData = Map($className$Page.toString -> JsBoolean(true)) - val getRelevantData = new FakeDataRetrievalAction(Some(CacheMap(cacheMapId, validData))) - val result = controller(getRelevantData).onPageLoad(NormalMode)(fakeRequest) + val userData = UserData(userDataId, Json.obj($className$Page.toString -> JsBoolean(true))) - contentAsString(result) mustBe viewAsString(form.fill(true)) + val application = applicationBuilder(userData = Some(userData)).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val view = application.injector.instanceOf[$className$View] + + val result = route(application, request).value + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view(form.fill(true), NormalMode)(fakeRequest, messages).toString } "redirect to the next page when valid data is submitted" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("value", "true")) - val result = controller().onSubmit(NormalMode)(postRequest) + val application = + applicationBuilder(userData = Some(emptyUserData)) + .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) + .build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", "true")) + + val result = route(application, request).value - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(onwardRoute.url) + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual onwardRoute.url } "return a Bad Request and errors when invalid data is submitted" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("value", "invalid value")) - val boundForm = form.bind(Map("value" -> "invalid value")) - val result = controller().onSubmit(NormalMode)(postRequest) + val application = applicationBuilder(userData = Some(emptyUserData)).build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", "")) + + val boundForm = form.bind(Map("value" -> "")) + + val view = application.injector.instanceOf[$className$View] - status(result) mustBe BAD_REQUEST - contentAsString(result) mustBe viewAsString(boundForm) + val result = route(application, request).value + + status(result) mustEqual BAD_REQUEST + + contentAsString(result) mustEqual + view(boundForm, NormalMode)(fakeRequest, messages).toString } "redirect to Session Expired for a GET if no existing data is found" in { - val result = controller(dontGetAnyData).onPageLoad(NormalMode)(fakeRequest) - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(routes.SessionExpiredController.onPageLoad().url) + val application = applicationBuilder(userData = None).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url } "redirect to Session Expired for a POST if no existing data is found" in { - val postRequest = fakeRequest.withFormUrlEncodedBody(("value", "true")) - val result = controller(dontGetAnyData).onSubmit(NormalMode)(postRequest) - status(result) mustBe SEE_OTHER - redirectLocation(result) mustBe Some(routes.SessionExpiredController.onPageLoad().url) + val application = applicationBuilder(userData = None).build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", "true")) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url } } } diff --git a/src/main/scaffolds/yesNoPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/yesNoPage/generated-test/views/$className$ViewSpec.scala index 278b96a4..ba17da50 100644 --- a/src/main/scaffolds/yesNoPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/yesNoPage/generated-test/views/$className$ViewSpec.scala @@ -1,11 +1,12 @@ package views -import play.api.data.Form import controllers.routes import forms.$className$FormProvider -import views.behaviours.YesNoViewBehaviours import models.NormalMode -import views.html.$className;format="decap"$ +import play.api.data.Form +import play.twirl.api.HtmlFormat +import views.behaviours.YesNoViewBehaviours +import views.html.$className$View class $className$ViewSpec extends YesNoViewBehaviours { @@ -13,16 +14,19 @@ class $className$ViewSpec extends YesNoViewBehaviours { val form = new $className$FormProvider()() - def createView = () => $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages) + "$className$ view" must { - def createViewUsingForm = (form: Form[_]) => $className;format="decap"$(frontendAppConfig, form, NormalMode)(fakeRequest, messages) + val application = applicationBuilder(userData = Some(emptyUserData)).build() - "$className$ view" must { + val view = application.injector.instanceOf[$className$View] + + def applyView(form: Form[_]): HtmlFormat.Appendable = + view.apply(form, NormalMode)(fakeRequest, messages) - behave like normalPage(createView, messageKeyPrefix) + behave like normalPage(applyView(form), messageKeyPrefix) - behave like pageWithBackLink(createView) + behave like pageWithBackLink(applyView(form)) - behave like yesNoPage(createViewUsingForm, messageKeyPrefix, routes.$className$Controller.onSubmit(NormalMode).url) + behave like yesNoPage(form, applyView, messageKeyPrefix, routes.$className$Controller.onSubmit(NormalMode).url) } } diff --git a/src/main/scaffolds/yesNoPage/migrations/$className__snake$.sh b/src/main/scaffolds/yesNoPage/migrations/$className__snake$.sh index 6ab4a31b..c0f6dae3 100644 --- a/src/main/scaffolds/yesNoPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/yesNoPage/migrations/$className__snake$.sh @@ -1,5 +1,6 @@ #!/bin/bash +echo "" echo "Applying migration $className;format="snake"$" echo "Adding routes to conf/app.routes" @@ -39,11 +40,11 @@ awk '/trait PageGenerators/ {\ print " Arbitrary($className$Page)";\ next }1' ../test/generators/PageGenerators.scala > tmp && mv tmp ../test/generators/PageGenerators.scala -echo "Adding to CacheMapGenerator" +echo "Adding to UserDataGenerator" awk '/val generators/ {\ print;\ print " arbitrary[($className$Page.type, JsValue)] ::";\ - next }1' ../test/generators/CacheMapGenerator.scala > tmp && mv tmp ../test/generators/CacheMapGenerator.scala + next }1' ../test/generators/UserDataGenerator.scala > tmp && mv tmp ../test/generators/UserDataGenerator.scala echo "Adding helper method to CheckYourAnswersHelper" awk '/class/ {\ From cef8f55deb1c703242c1328b2bfef17004347d05 Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Fri, 16 Nov 2018 08:27:07 +0000 Subject: [PATCH 03/23] Refactor User Answers Move away from using CacheMap, storing a Json object instead. Introduce Paths on each page, to let data be stored in more complex objects. In particular, this makes it easier to have looping sections of a journey (and loops wihtin loops etc.) --- .../actions/DataRetrievalAction.scala | 4 +- .../g8/app/models/MongoDateTimeFormats.scala | 22 ++ src/main/g8/app/models/UserAnswers.scala | 67 +++++- src/main/g8/app/models/UserData.scala | 41 ---- src/main/g8/app/models/package.scala | 81 +++++++ src/main/g8/app/pages/QuestionPage.scala | 8 +- .../app/repositories/SessionRepository.scala | 16 +- src/main/g8/test/base/SpecBase.scala | 13 +- .../CheckYourAnswersControllerSpec.scala | 4 +- .../controllers/IndexControllerSpec.scala | 2 +- .../SessionExpiredControllerSpec.scala | 2 +- .../UnauthorisedControllerSpec.scala | 2 +- .../controllers/actions/AuthActionSpec.scala | 14 +- .../actions/DataRetrievalActionSpec.scala | 4 +- .../actions/FakeDataRetrievalAction.scala | 8 +- .../actions/SessionActionSpec.scala | 4 +- src/main/g8/test/generators/Generators.scala | 2 +- .../generators/UserAnswersGenerator.scala | 36 ++++ .../test/generators/UserDataGenerator.scala | 30 --- .../models/MongoDateTimeFormatsSpec.scala | 35 +++ src/main/g8/test/models/RichJsValueSpec.scala | 202 ++++++++++++++++++ .../g8/test/navigation/NavigatorSpec.scala | 8 +- .../pages/behaviours/PageBehaviours.scala | 57 +++-- .../controllers/$className$Controller.scala | 10 +- .../intPage/app/pages/$className$Page.scala | 4 + .../$className$ControllerSpec.scala | 16 +- .../views/$className$ViewSpec.scala | 2 +- .../intPage/migrations/$className__snake$.sh | 4 +- .../controllers/$className$Controller.scala | 10 +- .../optionsPage/app/models/$className$.scala | 14 +- .../app/pages/$className$Page.scala | 3 + .../$className$ControllerSpec.scala | 16 +- .../models/$className$Spec.scala | 2 +- .../views/$className$ViewSpec.scala | 2 +- .../migrations/$className__snake$.sh | 4 +- .../$className$ControllerSpec.scala | 2 +- .../views/$className$ViewSpec.scala | 2 +- .../controllers/$className$Controller.scala | 10 +- .../app/pages/$className$Page.scala | 3 + .../$className$ControllerSpec.scala | 18 +- .../views/$className$ViewSpec.scala | 2 +- .../migrations/$className__snake$.sh | 4 +- .../controllers/$className$Controller.scala | 10 +- .../app/pages/$className$Page.scala | 4 + .../$className$ControllerSpec.scala | 16 +- .../views/$className$ViewSpec.scala | 2 +- .../migrations/$className__snake$.sh | 4 +- .../controllers/$className$Controller.scala | 10 +- .../yesNoPage/app/pages/$className$Page.scala | 4 + .../$className$ControllerSpec.scala | 16 +- .../views/$className$ViewSpec.scala | 2 +- .../migrations/$className__snake$.sh | 4 +- 52 files changed, 604 insertions(+), 258 deletions(-) create mode 100644 src/main/g8/app/models/MongoDateTimeFormats.scala delete mode 100644 src/main/g8/app/models/UserData.scala create mode 100644 src/main/g8/app/models/package.scala create mode 100644 src/main/g8/test/generators/UserAnswersGenerator.scala delete mode 100644 src/main/g8/test/generators/UserDataGenerator.scala create mode 100644 src/main/g8/test/models/MongoDateTimeFormatsSpec.scala create mode 100644 src/main/g8/test/models/RichJsValueSpec.scala diff --git a/src/main/g8/app/controllers/actions/DataRetrievalAction.scala b/src/main/g8/app/controllers/actions/DataRetrievalAction.scala index fd0c3b40..40b3ac89 100644 --- a/src/main/g8/app/controllers/actions/DataRetrievalAction.scala +++ b/src/main/g8/app/controllers/actions/DataRetrievalAction.scala @@ -20,8 +20,8 @@ class DataRetrievalActionImpl @Inject()( sessionRepository.get(request.identifier).map { case None => OptionalDataRequest(request.request, request.identifier, None) - case Some(data) => - OptionalDataRequest(request.request, request.identifier, Some(UserAnswers(data))) + case Some(userAnswers) => + OptionalDataRequest(request.request, request.identifier, Some(userAnswers)) } } } diff --git a/src/main/g8/app/models/MongoDateTimeFormats.scala b/src/main/g8/app/models/MongoDateTimeFormats.scala new file mode 100644 index 00000000..870b0b2d --- /dev/null +++ b/src/main/g8/app/models/MongoDateTimeFormats.scala @@ -0,0 +1,22 @@ +package models + +import java.time.{Instant, LocalDateTime, ZoneOffset} + +import play.api.libs.json._ + +trait MongoDateTimeFormats { + + implicit val localDateTimeRead: Reads[LocalDateTime] = + (__ \ "\$date").read[Long].map { + millis => + LocalDateTime.ofInstant(Instant.ofEpochMilli(millis), ZoneOffset.UTC) + } + + implicit val localDateTimeWrite: Writes[LocalDateTime] = new Writes[LocalDateTime] { + def writes(dateTime: LocalDateTime): JsValue = Json.obj( + "\$date" -> dateTime.atZone(ZoneOffset.UTC).toInstant.toEpochMilli + ) + } +} + +object MongoDateTimeFormats extends MongoDateTimeFormats diff --git a/src/main/g8/app/models/UserAnswers.scala b/src/main/g8/app/models/UserAnswers.scala index b8d537fe..5db7afa4 100644 --- a/src/main/g8/app/models/UserAnswers.scala +++ b/src/main/g8/app/models/UserAnswers.scala @@ -1,28 +1,75 @@ package models +import java.time.LocalDateTime + import pages._ import play.api.libs.json._ -case class UserAnswers(userData: UserData) extends Enumerable.Implicits { +import scala.util.{Failure, Success, Try} + +final case class UserAnswers( + id: String, + data: JsObject = Json.obj(), + lastUpdated: LocalDateTime = LocalDateTime.now + ) { def get[A](page: QuestionPage[A])(implicit rds: Reads[A]): Option[A] = - userData.getEntry[A](page) + Reads.optionNoError(Reads.at(page.path)).reads(data).getOrElse(None) + + def set[A](page: QuestionPage[A], value: A)(implicit writes: Writes[A]): Try[UserAnswers] = { - def set[A](page: QuestionPage[A], value: A)(implicit writes: Writes[A]): UserAnswers = { - val updatedAnswers = UserAnswers(userData copy (data = userData.data + (page.toString -> Json.toJson(value)))) + val updatedData = data.setObject(page.path, Json.toJson(value)) match { + case JsSuccess(jsValue, _) => + Success(jsValue) + case JsError(errors) => + Failure(JsResultException(errors)) + } - page.cleanup(Some(value), updatedAnswers) + updatedData.flatMap { + d => + val updatedAnswers = copy (data = d) + page.cleanup(Some(value), updatedAnswers) + } } - def remove[A](page: QuestionPage[A]): UserAnswers = { - val updatedAnswers = UserAnswers(userData copy (data = userData.data - page)) + def remove[A](page: QuestionPage[A]): Try[UserAnswers] = { - page.cleanup(None, updatedAnswers) + val updatedData = data.setObject(page.path, JsNull) match { + case JsSuccess(jsValue, _) => + Success(jsValue) + case JsError(_) => + Success(data) + } + + updatedData.flatMap { + d => + val updatedAnswers = copy (data = d) + page.cleanup(None, updatedAnswers) + } } } object UserAnswers { - def apply(cacheId: String): UserAnswers = - UserAnswers(new UserData(cacheId, Json.obj())) + implicit lazy val reads: Reads[UserAnswers] = { + + import play.api.libs.functional.syntax._ + + ( + (__ \ "_id").read[String] and + (__ \ "data").read[JsObject] and + (__ \ "lastUpdated").read(MongoDateTimeFormats.localDateTimeRead) + ) (UserAnswers.apply _) + } + + implicit lazy val writes: OWrites[UserAnswers] = { + + import play.api.libs.functional.syntax._ + + ( + (__ \ "_id").write[String] and + (__ \ "data").write[JsObject] and + (__ \ "lastUpdated").write(MongoDateTimeFormats.localDateTimeWrite) + ) (unlift(UserAnswers.unapply)) + } } diff --git a/src/main/g8/app/models/UserData.scala b/src/main/g8/app/models/UserData.scala deleted file mode 100644 index 60fe2319..00000000 --- a/src/main/g8/app/models/UserData.scala +++ /dev/null @@ -1,41 +0,0 @@ -package models - -import java.time.LocalDateTime - -import play.api.libs.json.{JsObject, OWrites, Reads, __} - -case class UserData( - id: String, - data: JsObject, - lastUpdated: LocalDateTime = LocalDateTime.now - ) { - - def getEntry[T](key: String)(implicit reads: Reads[T]): Option[T] = { - (data \ key).validate[T].asOpt - } -} - -object UserData { - - implicit lazy val reads: Reads[UserData] = { - - import play.api.libs.functional.syntax._ - - ( - (__ \ "_id").read[String] and - (__ \ "data").read[JsObject] and - (__ \ "lastUpdated").read[LocalDateTime] - ) (UserData.apply _) - } - - implicit lazy val writes: OWrites[UserData] = { - - import play.api.libs.functional.syntax._ - - ( - (__ \ "_id").write[String] and - (__ \ "data").write[JsObject] and - (__ \ "lastUpdated").write[LocalDateTime] - ) (unlift(UserData.unapply)) - } -} diff --git a/src/main/g8/app/models/package.scala b/src/main/g8/app/models/package.scala new file mode 100644 index 00000000..7cea9a80 --- /dev/null +++ b/src/main/g8/app/models/package.scala @@ -0,0 +1,81 @@ +import play.api.libs.json._ + +package object models { + + implicit class RichJsObject(jsObject: JsObject) { + + def setObject(path: JsPath, value: JsValue): JsResult[JsObject] = + jsObject.set(path, value).flatMap(_.validate[JsObject]) + } + + implicit class RichJsValue(jsValue: JsValue) { + + def set(path: JsPath, value: JsValue): JsResult[JsValue] = + (path.path, jsValue) match { + + case (Nil, _) => + JsError("path cannot be empty") + + case ((_: RecursiveSearch) :: _, _) => + JsError("recursive search not supported") + + case ((n: IdxPathNode) :: Nil, _) => + setIndexNode(n, jsValue, value) + + case ((n: KeyPathNode) :: Nil, _) => + setKeyNode(n, jsValue, value) + + case (first :: second :: rest, oldValue) => + Reads.optionNoError(Reads.at[JsValue](JsPath(first :: Nil))) + .reads(oldValue).flatMap { + opt => + + opt.map(JsSuccess(_)).getOrElse { + second match { + case _: KeyPathNode => + JsSuccess(Json.obj()) + case _: IdxPathNode => + JsSuccess(Json.arr()) + case _: RecursiveSearch => + JsError("recursive search is not supported") + } + }.flatMap { + _.set(JsPath(second :: rest), value).flatMap { + newValue => + oldValue.set(JsPath(first :: Nil), newValue) + } + } + } + } + + private def setIndexNode(node: IdxPathNode, oldValue: JsValue, newValue: JsValue): JsResult[JsValue] = { + + val index = node.idx + + oldValue match { + case oldValue: JsArray if index >= 0 && index <= oldValue.value.length => + if (index == oldValue.value.length) { + JsSuccess(oldValue.append(newValue)) + } else { + JsSuccess(JsArray(oldValue.value.updated(index, newValue))) + } + case oldValue: JsArray => + JsError(s"array index out of bounds: \$index, \$oldValue") + case _ => + JsError(s"cannot set an index on \$oldValue") + } + } + + private def setKeyNode(node: KeyPathNode, oldValue: JsValue, newValue: JsValue): JsResult[JsValue] = { + + val key = node.key + + oldValue match { + case oldValue: JsObject => + JsSuccess(oldValue + (key -> newValue)) + case _ => + JsError(s"cannot set a key on \$oldValue") + } + } + } +} diff --git a/src/main/g8/app/pages/QuestionPage.scala b/src/main/g8/app/pages/QuestionPage.scala index c80ee674..06bc5da3 100644 --- a/src/main/g8/app/pages/QuestionPage.scala +++ b/src/main/g8/app/pages/QuestionPage.scala @@ -1,8 +1,14 @@ package pages import models.UserAnswers +import play.api.libs.json.JsPath + +import scala.util.{Success, Try} trait QuestionPage[A] extends Page { - def cleanup(value: Option[A], userAnswers: UserAnswers): UserAnswers = userAnswers + def path: JsPath + + def cleanup(value: Option[A], userAnswers: UserAnswers): Try[UserAnswers] = + Success(userAnswers) } diff --git a/src/main/g8/app/repositories/SessionRepository.scala b/src/main/g8/app/repositories/SessionRepository.scala index 646c5e0d..cbb59d88 100644 --- a/src/main/g8/app/repositories/SessionRepository.scala +++ b/src/main/g8/app/repositories/SessionRepository.scala @@ -4,7 +4,7 @@ import java.time.LocalDateTime import akka.stream.Materializer import javax.inject.Inject -import models.UserData +import models.UserAnswers import play.api.Configuration import play.api.libs.json._ import play.modules.reactivemongo.ReactiveMongoApi @@ -39,17 +39,17 @@ class DefaultSessionRepository @Inject()( _.indexesManager.ensure(lastUpdatedIndex) }.map(_ => ()) - override def get(id: String): Future[Option[UserData]] = - collection.flatMap(_.find(Json.obj("_id" -> id), None).one[UserData]) + override def get(id: String): Future[Option[UserAnswers]] = + collection.flatMap(_.find(Json.obj("_id" -> id), None).one[UserAnswers]) - override def set(userData: UserData): Future[Boolean] = { + override def set(userAnswers: UserAnswers): Future[Boolean] = { val selector = Json.obj( - "_id" -> userData.id + "_id" -> userAnswers.id ) val modifier = Json.obj( - "\$set" -> (userData copy (lastUpdated = LocalDateTime.now)) + "\$set" -> (userAnswers copy (lastUpdated = LocalDateTime.now)) ) collection.flatMap { @@ -65,7 +65,7 @@ trait SessionRepository { val started: Future[Unit] - def get(id: String): Future[Option[UserData]] + def get(id: String): Future[Option[UserAnswers]] - def set(userData: UserData): Future[Boolean] + def set(userAnswers: UserAnswers): Future[Boolean] } diff --git a/src/main/g8/test/base/SpecBase.scala b/src/main/g8/test/base/SpecBase.scala index bc883a06..3e4ceee2 100644 --- a/src/main/g8/test/base/SpecBase.scala +++ b/src/main/g8/test/base/SpecBase.scala @@ -2,7 +2,8 @@ package base import config.FrontendAppConfig import controllers.actions._ -import models.UserData +import models.UserAnswers +import org.scalatest.TryValues import org.scalatestplus.play.PlaySpec import org.scalatestplus.play.guice._ import play.api.i18n.{Messages, MessagesApi} @@ -11,11 +12,11 @@ import play.api.inject.{Injector, bind} import play.api.libs.json.Json import play.api.test.FakeRequest -trait SpecBase extends PlaySpec with GuiceOneAppPerSuite { +trait SpecBase extends PlaySpec with GuiceOneAppPerSuite with TryValues { - val userDataId = "id" + val userAnswersId = "id" - def emptyUserData = UserData(userDataId, Json.obj()) + def emptyUserAnswers = UserAnswers(userAnswersId, Json.obj()) def injector: Injector = app.injector @@ -27,11 +28,11 @@ trait SpecBase extends PlaySpec with GuiceOneAppPerSuite { def messages: Messages = messagesApi.preferred(fakeRequest) - protected def applicationBuilder(userData: Option[UserData] = None): GuiceApplicationBuilder = + protected def applicationBuilder(userAnswers: Option[UserAnswers] = None): GuiceApplicationBuilder = new GuiceApplicationBuilder() .overrides( bind[DataRequiredAction].to[DataRequiredActionImpl], bind[IdentifierAction].to[FakeIdentifierAction], - bind[DataRetrievalAction].toInstance(new FakeDataRetrievalAction(userData)) + bind[DataRetrievalAction].toInstance(new FakeDataRetrievalAction(userAnswers)) ) } diff --git a/src/main/g8/test/controllers/CheckYourAnswersControllerSpec.scala b/src/main/g8/test/controllers/CheckYourAnswersControllerSpec.scala index 32bbcc09..06b13f1d 100644 --- a/src/main/g8/test/controllers/CheckYourAnswersControllerSpec.scala +++ b/src/main/g8/test/controllers/CheckYourAnswersControllerSpec.scala @@ -12,7 +12,7 @@ class CheckYourAnswersControllerSpec extends SpecBase { "return OK and the correct view for a GET" in { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val request = FakeRequest(GET, routes.CheckYourAnswersController.onPageLoad().url) @@ -28,7 +28,7 @@ class CheckYourAnswersControllerSpec extends SpecBase { "redirect to Session Expired for a GET if no existing data is found" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val request = FakeRequest(GET, routes.CheckYourAnswersController.onPageLoad().url) diff --git a/src/main/g8/test/controllers/IndexControllerSpec.scala b/src/main/g8/test/controllers/IndexControllerSpec.scala index 384e3858..49e07b0f 100644 --- a/src/main/g8/test/controllers/IndexControllerSpec.scala +++ b/src/main/g8/test/controllers/IndexControllerSpec.scala @@ -11,7 +11,7 @@ class IndexControllerSpec extends SpecBase { "return OK and the correct view for a GET" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val request = FakeRequest(GET, routes.IndexController.onPageLoad().url) diff --git a/src/main/g8/test/controllers/SessionExpiredControllerSpec.scala b/src/main/g8/test/controllers/SessionExpiredControllerSpec.scala index e88ae82f..cfbd4873 100644 --- a/src/main/g8/test/controllers/SessionExpiredControllerSpec.scala +++ b/src/main/g8/test/controllers/SessionExpiredControllerSpec.scala @@ -11,7 +11,7 @@ class SessionExpiredControllerSpec extends SpecBase { "return OK and the correct view for a GET" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val request = FakeRequest(GET, routes.SessionExpiredController.onPageLoad().url) diff --git a/src/main/g8/test/controllers/UnauthorisedControllerSpec.scala b/src/main/g8/test/controllers/UnauthorisedControllerSpec.scala index 5121b5ff..75976172 100644 --- a/src/main/g8/test/controllers/UnauthorisedControllerSpec.scala +++ b/src/main/g8/test/controllers/UnauthorisedControllerSpec.scala @@ -11,7 +11,7 @@ class UnauthorisedControllerSpec extends SpecBase { "return OK and the correct view for a GET" in { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val request = FakeRequest(GET, routes.UnauthorisedController.onPageLoad().url) diff --git a/src/main/g8/test/controllers/actions/AuthActionSpec.scala b/src/main/g8/test/controllers/actions/AuthActionSpec.scala index e0008c3c..6d981aaa 100644 --- a/src/main/g8/test/controllers/actions/AuthActionSpec.scala +++ b/src/main/g8/test/controllers/actions/AuthActionSpec.scala @@ -25,7 +25,7 @@ class AuthActionSpec extends SpecBase { "redirect the user to log in " in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val bodyParsers = application.injector.instanceOf[BodyParsers.Default] @@ -43,7 +43,7 @@ class AuthActionSpec extends SpecBase { "redirect the user to log in " in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val bodyParsers = application.injector.instanceOf[BodyParsers.Default] @@ -61,7 +61,7 @@ class AuthActionSpec extends SpecBase { "redirect the user to the unauthorised page" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val bodyParsers = application.injector.instanceOf[BodyParsers.Default] @@ -79,7 +79,7 @@ class AuthActionSpec extends SpecBase { "redirect the user to the unauthorised page" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val bodyParsers = application.injector.instanceOf[BodyParsers.Default] @@ -97,7 +97,7 @@ class AuthActionSpec extends SpecBase { "redirect the user to the unauthorised page" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val bodyParsers = application.injector.instanceOf[BodyParsers.Default] @@ -115,7 +115,7 @@ class AuthActionSpec extends SpecBase { "redirect the user to the unauthorised page" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val bodyParsers = application.injector.instanceOf[BodyParsers.Default] @@ -133,7 +133,7 @@ class AuthActionSpec extends SpecBase { "redirect the user to the unauthorised page" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val bodyParsers = application.injector.instanceOf[BodyParsers.Default] diff --git a/src/main/g8/test/controllers/actions/DataRetrievalActionSpec.scala b/src/main/g8/test/controllers/actions/DataRetrievalActionSpec.scala index 975a529e..acbfe8ae 100644 --- a/src/main/g8/test/controllers/actions/DataRetrievalActionSpec.scala +++ b/src/main/g8/test/controllers/actions/DataRetrievalActionSpec.scala @@ -1,7 +1,7 @@ package controllers.actions import base.SpecBase -import models.UserData +import models.UserAnswers import models.requests.{IdentifierRequest, OptionalDataRequest} import org.mockito.Mockito._ import org.scalatest.concurrent.ScalaFutures @@ -41,7 +41,7 @@ class DataRetrievalActionSpec extends SpecBase with MockitoSugar with ScalaFutur "build a userAnswers object and add it to the request" in { val sessionRepository = mock[SessionRepository] - when(sessionRepository.get("id")) thenReturn Future(Some(new UserData("id", Json.obj()))) + when(sessionRepository.get("id")) thenReturn Future(Some(new UserAnswers("id"))) val action = new Harness(sessionRepository) val futureResult = action.callTransform(new IdentifierRequest(fakeRequest, "id")) diff --git a/src/main/g8/test/controllers/actions/FakeDataRetrievalAction.scala b/src/main/g8/test/controllers/actions/FakeDataRetrievalAction.scala index 37c04f9e..45a6cba7 100644 --- a/src/main/g8/test/controllers/actions/FakeDataRetrievalAction.scala +++ b/src/main/g8/test/controllers/actions/FakeDataRetrievalAction.scala @@ -1,18 +1,18 @@ package controllers.actions +import models.UserAnswers import models.requests.{IdentifierRequest, OptionalDataRequest} -import models.{UserAnswers, UserData} import scala.concurrent.{ExecutionContext, Future} -class FakeDataRetrievalAction(dataToReturn: Option[UserData]) extends DataRetrievalAction { +class FakeDataRetrievalAction(dataToReturn: Option[UserAnswers]) extends DataRetrievalAction { override protected def transform[A](request: IdentifierRequest[A]): Future[OptionalDataRequest[A]] = dataToReturn match { case None => Future(OptionalDataRequest(request.request, request.identifier, None)) - case Some(userData) => - Future(OptionalDataRequest(request.request, request.identifier, Some(new UserAnswers(userData)))) + case Some(userAnswers) => + Future(OptionalDataRequest(request.request, request.identifier, Some(userAnswers))) } override protected implicit val executionContext: ExecutionContext = diff --git a/src/main/g8/test/controllers/actions/SessionActionSpec.scala b/src/main/g8/test/controllers/actions/SessionActionSpec.scala index dd006aea..e013533b 100644 --- a/src/main/g8/test/controllers/actions/SessionActionSpec.scala +++ b/src/main/g8/test/controllers/actions/SessionActionSpec.scala @@ -19,7 +19,7 @@ class SessionActionSpec extends SpecBase { "redirect to the session expired page" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val bodyParsers = application.injector.instanceOf[BodyParsers.Default] @@ -38,7 +38,7 @@ class SessionActionSpec extends SpecBase { "perform the action" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val bodyParsers = application.injector.instanceOf[BodyParsers.Default] diff --git a/src/main/g8/test/generators/Generators.scala b/src/main/g8/test/generators/Generators.scala index 49ff95f2..733a6e98 100644 --- a/src/main/g8/test/generators/Generators.scala +++ b/src/main/g8/test/generators/Generators.scala @@ -5,7 +5,7 @@ import Gen._ import Arbitrary._ import play.api.libs.json.{JsBoolean, JsNumber, JsString} -trait Generators extends UserDataGenerator with PageGenerators with ModelGenerators with UserAnswersEntryGenerators { +trait Generators extends UserAnswersGenerator with PageGenerators with ModelGenerators with UserAnswersEntryGenerators { implicit val dontShrink: Shrink[String] = Shrink.shrinkAny diff --git a/src/main/g8/test/generators/UserAnswersGenerator.scala b/src/main/g8/test/generators/UserAnswersGenerator.scala new file mode 100644 index 00000000..a86d9a02 --- /dev/null +++ b/src/main/g8/test/generators/UserAnswersGenerator.scala @@ -0,0 +1,36 @@ +package generators + +import models.UserAnswers +import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.{Arbitrary, Gen} +import org.scalatest.TryValues +import pages._ +import play.api.libs.json.{JsPath, JsValue, Json} + +trait UserAnswersGenerator extends TryValues { + self: Generators => + + val generators: Seq[Gen[(QuestionPage[_], JsValue)]] = + Nil + + implicit lazy val arbitraryUserData: Arbitrary[UserAnswers] = { + + import models._ + + Arbitrary { + for { + id <- nonEmptyString + data <- generators match { + case Nil => Gen.const(Map[QuestionPage[_], JsValue]()) + case _ => Gen.mapOf(oneOf(generators)) + } + } yield UserAnswers ( + id = id, + data = data.foldLeft(Json.obj()) { + case (obj, (path, value)) => + obj.setObject(path.path, value).get + } + ) + } + } +} \ No newline at end of file diff --git a/src/main/g8/test/generators/UserDataGenerator.scala b/src/main/g8/test/generators/UserDataGenerator.scala deleted file mode 100644 index 86cdb204..00000000 --- a/src/main/g8/test/generators/UserDataGenerator.scala +++ /dev/null @@ -1,30 +0,0 @@ -package generators - -import models.UserData -import org.scalacheck.Arbitrary.arbitrary -import org.scalacheck.{Arbitrary, Gen} -import pages._ -import play.api.libs.json.{JsValue, Json} - -trait UserDataGenerator { - self: Generators => - - val generators: Seq[Gen[(Page, JsValue)]] = - Nil - - implicit lazy val arbitraryUserData: Arbitrary[UserData] = - Arbitrary { - for { - cacheId <- nonEmptyString - data <- generators match { - case Nil => Gen.const(Map[Page, JsValue]()) - case _ => Gen.mapOf(oneOf(generators)) - } - } yield UserData( - cacheId, - data.map { - case (k, v) => Json.obj(k.toString -> v) - }.foldLeft(Json.obj())(_ ++ _) - ) - } -} diff --git a/src/main/g8/test/models/MongoDateTimeFormatsSpec.scala b/src/main/g8/test/models/MongoDateTimeFormatsSpec.scala new file mode 100644 index 00000000..2f9c5051 --- /dev/null +++ b/src/main/g8/test/models/MongoDateTimeFormatsSpec.scala @@ -0,0 +1,35 @@ +package models + +import java.time.{LocalDate, LocalDateTime} + +import org.scalatest.{FreeSpec, MustMatchers, OptionValues} +import play.api.libs.json.Json + +class MongoDateTimeFormatsSpec extends FreeSpec with MustMatchers with OptionValues with MongoDateTimeFormats { + + "a LocalDateTime" - { + + val date = LocalDate.of(2018, 2, 1).atStartOfDay + + val dateMillis = 1517443200000L + + val json = Json.obj( + "\$date" -> dateMillis + ) + + "must serialise to json" in { + val result = Json.toJson(date) + result mustEqual json + } + + "must deserialise from json" in { + val result = json.as[LocalDateTime] + result mustEqual date + } + + "must serialise/deserialise to the same value" in { + val result = Json.toJson(date).as[LocalDateTime] + result mustEqual date + } + } +} diff --git a/src/main/g8/test/models/RichJsValueSpec.scala b/src/main/g8/test/models/RichJsValueSpec.scala new file mode 100644 index 00000000..f478534d --- /dev/null +++ b/src/main/g8/test/models/RichJsValueSpec.scala @@ -0,0 +1,202 @@ +package models + +import org.scalacheck.{Gen, Shrink} +import org.scalatest.prop.PropertyChecks +import org.scalatest.{FreeSpec, MustMatchers, OptionValues} +import play.api.libs.json._ + +class RichJsValueSpec extends FreeSpec with MustMatchers with PropertyChecks with OptionValues { + + implicit val dontShrink: Shrink[String] = Shrink.shrinkAny + + val nonEmptyAlphaStr: Gen[String] = Gen.alphaStr.suchThat(_.nonEmpty) + + "set" - { + + "must return an error if the path is empty" in { + + val value = Json.obj() + + value.set(JsPath, Json.obj()) mustEqual JsError("path cannot be empty") + } + + "must set a value on a JsObject" in { + + val gen = for { + originalKey <- nonEmptyAlphaStr + originalValue <- nonEmptyAlphaStr + pathKey <- nonEmptyAlphaStr suchThat (_ != originalKey) + newValue <- nonEmptyAlphaStr + } yield (originalKey, originalValue, pathKey, newValue) + + forAll(gen) { + case (originalKey, originalValue, pathKey, newValue) => + + val value = Json.obj(originalKey -> originalValue) + + val path = JsPath \ pathKey + + value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.obj(originalKey -> originalValue, pathKey -> newValue)) + } + } + + "must set a nested value on a JsObject" in { + + val value = Json.obj( + "foo" -> Json.obj() + ) + + val path = JsPath \ "foo" \ "bar" + + value.set(path, JsString("baz")).asOpt.value mustEqual Json.obj( + "foo" -> Json.obj( + "bar" -> "baz" + ) + ) + } + + "must add a value to an empty JsArray" in { + + forAll(nonEmptyAlphaStr) { + newValue => + + val value = Json.arr() + + val path = JsPath \ 0 + + value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.arr(newValue)) + } + } + + "must add a value to the end of a JsArray" in { + + forAll(nonEmptyAlphaStr, nonEmptyAlphaStr) { + (oldValue, newValue) => + + val value = Json.arr(oldValue) + + val path = JsPath \ 1 + + value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.arr(oldValue, newValue)) + } + } + + "must change a value in an existing JsArray" in { + + forAll(nonEmptyAlphaStr, nonEmptyAlphaStr, nonEmptyAlphaStr) { + (firstValue, secondValue, newValue) => + + val value = Json.arr(firstValue, secondValue) + + val path = JsPath \ 0 + + value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.arr(newValue, secondValue)) + } + } + + "must set a nested value on a JsArray" in { + + val value = Json.arr(Json.arr("foo")) + + val path = JsPath \ 0 \ 0 + + value.set(path, JsString("bar")).asOpt.value mustEqual Json.arr(Json.arr("bar")) + } + + "must change the value of an existing key" in { + + val gen = for { + originalKey <- nonEmptyAlphaStr + originalValue <- nonEmptyAlphaStr + newValue <- nonEmptyAlphaStr + } yield (originalKey, originalValue, newValue) + + forAll(gen) { + case (pathKey, originalValue, newValue) => + + val value = Json.obj(pathKey -> originalValue) + + val path = JsPath \ pathKey + + value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.obj(pathKey -> newValue)) + } + } + + "must return an error when trying to set a key on a non-JsObject" in { + + val value = Json.arr() + + val path = JsPath \ "foo" + + value.set(path, JsString("bar")) mustEqual JsError(s"cannot set a key on \$value") + } + + "must return an error when trying to set an index on a non-JsArray" in { + + val value = Json.obj() + + val path = JsPath \ 0 + + value.set(path, JsString("bar")) mustEqual JsError(s"cannot set an index on \$value") + } + + "must return an error when trying to set an index other than zero on an empty array" in { + + val value = Json.arr() + + val path = JsPath \ 1 + + value.set(path, JsString("bar")) mustEqual JsError("array index out of bounds: 1, []") + } + + "must return an error when trying to set an index out of bounds" in { + + val value = Json.arr("bar", "baz") + + val path = JsPath \ 3 + + value.set(path, JsString("fork")) mustEqual JsError("array index out of bounds: 3, [\"bar\",\"baz\"]") + } + + "must set into an array which does not exist" in { + + val value = Json.obj() + + val path = JsPath \ "foo" \ 0 + + value.set(path, JsString("bar")) mustEqual JsSuccess(Json.obj( + "foo" -> Json.arr("bar") + )) + } + + "must set into an object which does not exist" in { + + val value = Json.obj() + + val path = JsPath \ "foo" \ "bar" + + value.set(path, JsString("baz")) mustEqual JsSuccess(Json.obj( + "foo" -> Json.obj( + "bar" -> "baz" + ) + )) + } + + "must set nested objects and arrays" in { + + val value = Json.obj() + + val path = JsPath \ "foo" \ 0 \ "bar" \ 0 + + value.set(path, JsString("baz")) mustEqual JsSuccess(Json.obj( + "foo" -> Json.arr( + Json.obj( + "bar" -> Json.arr( + "baz" + ) + ) + ) + )) + } + } +} diff --git a/src/main/g8/test/navigation/NavigatorSpec.scala b/src/main/g8/test/navigation/NavigatorSpec.scala index 4f26a12b..e3edd51c 100644 --- a/src/main/g8/test/navigation/NavigatorSpec.scala +++ b/src/main/g8/test/navigation/NavigatorSpec.scala @@ -1,13 +1,11 @@ package navigation import base.SpecBase -import org.mockito.Mockito._ -import org.scalatest.mockito.MockitoSugar import controllers.routes import pages._ import models._ -class NavigatorSpec extends SpecBase with MockitoSugar { +class NavigatorSpec extends SpecBase { val navigator = new Navigator @@ -18,7 +16,7 @@ class NavigatorSpec extends SpecBase with MockitoSugar { "go to Index from a page that doesn't exist in the route map" in { case object UnknownPage extends Page - navigator.nextPage(UnknownPage, NormalMode)(mock[UserAnswers]) mustBe routes.IndexController.onPageLoad() + navigator.nextPage(UnknownPage, NormalMode)(UserAnswers("id")) mustBe routes.IndexController.onPageLoad() } } @@ -27,7 +25,7 @@ class NavigatorSpec extends SpecBase with MockitoSugar { "go to CheckYourAnswers from a page that doesn't exist in the edit route map" in { case object UnknownPage extends Page - navigator.nextPage(UnknownPage, CheckMode)(mock[UserAnswers]) mustBe routes.CheckYourAnswersController.onPageLoad() + navigator.nextPage(UnknownPage, CheckMode)(UserAnswers("id")) mustBe routes.CheckYourAnswersController.onPageLoad() } } } diff --git a/src/main/g8/test/pages/behaviours/PageBehaviours.scala b/src/main/g8/test/pages/behaviours/PageBehaviours.scala index b4b3189c..b82b57c7 100644 --- a/src/main/g8/test/pages/behaviours/PageBehaviours.scala +++ b/src/main/g8/test/pages/behaviours/PageBehaviours.scala @@ -1,15 +1,15 @@ package pages.behaviours import generators.Generators -import models.{UserAnswers, UserData} +import models.UserAnswers import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.{Arbitrary, Gen} import org.scalatest.prop.PropertyChecks -import org.scalatest.{MustMatchers, OptionValues, WordSpec} +import org.scalatest.{MustMatchers, OptionValues, TryValues, WordSpec} import pages.QuestionPage import play.api.libs.json._ -trait PageBehaviours extends WordSpec with MustMatchers with PropertyChecks with Generators with OptionValues { +trait PageBehaviours extends WordSpec with MustMatchers with PropertyChecks with Generators with OptionValues with TryValues { class BeRetrievable[A] { def apply[P <: QuestionPage[A]](genP: Gen[P])(implicit ev1: Arbitrary[A], ev2: Format[A]): Unit = { @@ -21,18 +21,14 @@ trait PageBehaviours extends WordSpec with MustMatchers with PropertyChecks with "the question has not been answered" in { val gen = for { - page <- genP - userData <- arbitrary[UserData] - } yield (page, userData copy (data = userData.data - page.toString)) + page <- genP + userAnswers <- arbitrary[UserAnswers] + } yield (page, userAnswers.remove(page).success.value) forAll(gen) { - case (page, userData) => + case (page, userAnswers) => - whenever(!userData.data.keys.contains(page.toString)) { - - val userAnswers = UserAnswers(userData) - userAnswers.get(page) must be(empty) - } + userAnswers.get(page) must be(empty) } } } @@ -45,15 +41,14 @@ trait PageBehaviours extends WordSpec with MustMatchers with PropertyChecks with "the question has been answered" in { val gen = for { - page <- genP - savedValue <- arbitrary[A] - userData <- arbitrary[UserData] - } yield (page, savedValue, userData copy (data = userData.data + (page.toString -> Json.toJson(savedValue)))) + page <- genP + savedValue <- arbitrary[A] + userAnswers <- arbitrary[UserAnswers] + } yield (page, savedValue, userAnswers.set(page, savedValue).success.value) forAll(gen) { - case (page, savedValue, userData) => + case (page, savedValue, userAnswers) => - val userAnswers = UserAnswers(userData) userAnswers.get(page).value mustEqual savedValue } } @@ -68,16 +63,15 @@ trait PageBehaviours extends WordSpec with MustMatchers with PropertyChecks with "be able to be set on UserAnswers" in { val gen = for { - page <- genP - newValue <- arbitrary[A] - userData <- arbitrary[UserData] - } yield (page, newValue, userData) + page <- genP + newValue <- arbitrary[A] + userAnswers <- arbitrary[UserAnswers] + } yield (page, newValue, userAnswers) forAll(gen) { - case (page, newValue, userData) => + case (page, newValue, userAnswers) => - val userAnswers = UserAnswers(userData) - val updatedAnswers = userAnswers.set(page, newValue) + val updatedAnswers = userAnswers.set(page, newValue).success.value updatedAnswers.get(page).value mustEqual newValue } } @@ -90,16 +84,15 @@ trait PageBehaviours extends WordSpec with MustMatchers with PropertyChecks with "be able to be removed from UserAnswers" in { val gen = for { - page <- genP - savedValue <- arbitrary[A] - userData <- arbitrary[UserData] - } yield (page, userData copy (data = userData.data + (page.toString -> Json.toJson(savedValue)))) + page <- genP + savedValue <- arbitrary[A] + userAnswers <- arbitrary[UserAnswers] + } yield (page, userAnswers.set(page, savedValue).success.value) forAll(gen) { - case (page, userData) => + case (page, userAnswers) => - val userAnswers = UserAnswers(userData) - val updatedAnswers = userAnswers.remove(page) + val updatedAnswers = userAnswers.remove(page).success.value updatedAnswers.get(page) must be(empty) } } diff --git a/src/main/scaffolds/intPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/intPage/app/controllers/$className$Controller.scala index dd96c863..5c7d8d6e 100644 --- a/src/main/scaffolds/intPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/intPage/app/controllers/$className$Controller.scala @@ -48,12 +48,10 @@ class $className$Controller @Inject()( Future.successful(BadRequest(view(formWithErrors, mode))), value => { - val updatedAnswers = request.userAnswers.set($className$Page, value) - - sessionRepository.set(updatedAnswers.userData).map( - _ => - Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) - ) + for { + updatedAnswers <- Future.fromTry(request.userAnswers.set($className$Page, value)) + _ <- sessionRepository.set(updatedAnswers) + } yield Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) } ) } diff --git a/src/main/scaffolds/intPage/app/pages/$className$Page.scala b/src/main/scaffolds/intPage/app/pages/$className$Page.scala index a8c7e797..237ce6aa 100644 --- a/src/main/scaffolds/intPage/app/pages/$className$Page.scala +++ b/src/main/scaffolds/intPage/app/pages/$className$Page.scala @@ -1,6 +1,10 @@ package pages +import play.api.libs.json.JsPath + case object $className$Page extends QuestionPage[Int] { + override def path: JsPath = JsPath \ toString + override def toString: String = "$className;format="decap"$" } diff --git a/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala index 4918120b..96b91100 100644 --- a/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala @@ -2,7 +2,7 @@ package controllers import base.SpecBase import forms.$className$FormProvider -import models.{NormalMode, UserData} +import models.{NormalMode, UserAnswers} import navigation.{FakeNavigator, Navigator} import pages.$className$Page import play.api.inject.bind @@ -27,7 +27,7 @@ class $className$ControllerSpec extends SpecBase { "return OK and the correct view for a GET" in { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -43,9 +43,9 @@ class $className$ControllerSpec extends SpecBase { "populate the view correctly on a GET when the question has previously been answered" in { - val userData = UserData(userDataId, Json.obj($className$Page.toString -> JsNumber(validAnswer))) + val userAnswers = UserAnswers(userAnswersId).set($className$Page, validAnswer).success.value - val application = applicationBuilder(userData = Some(userData)).build() + val application = applicationBuilder(userAnswers = Some(userAnswers)).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -62,7 +62,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to the next page when valid data is submitted" in { val application = - applicationBuilder(userData = Some(emptyUserData)) + applicationBuilder(userAnswers = Some(emptyUserAnswers)) .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) .build() @@ -79,7 +79,7 @@ class $className$ControllerSpec extends SpecBase { "return a Bad Request and errors when invalid data is submitted" in { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val request = FakeRequest(POST, $className;format="decap"$Route) @@ -99,7 +99,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to Session Expired for a GET if no existing data is found" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -111,7 +111,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to Session Expired for a POST if no existing data is found" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val request = FakeRequest(POST, $className;format="decap"$Route) diff --git a/src/main/scaffolds/intPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/intPage/generated-test/views/$className$ViewSpec.scala index 4d4ed6b9..5d3f28c5 100644 --- a/src/main/scaffolds/intPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/intPage/generated-test/views/$className$ViewSpec.scala @@ -16,7 +16,7 @@ class $className$ViewSpec extends IntViewBehaviours { "$className$View view" must { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val view = application.injector.instanceOf[$className$View] diff --git a/src/main/scaffolds/intPage/migrations/$className__snake$.sh b/src/main/scaffolds/intPage/migrations/$className__snake$.sh index c63d9fe7..f37473c5 100644 --- a/src/main/scaffolds/intPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/intPage/migrations/$className__snake$.sh @@ -43,11 +43,11 @@ awk '/trait PageGenerators/ {\ print " Arbitrary($className$Page)";\ next }1' ../test/generators/PageGenerators.scala > tmp && mv tmp ../test/generators/PageGenerators.scala -echo "Adding to UserDataGenerator" +echo "Adding to UserAnswersGenerator" awk '/val generators/ {\ print;\ print " arbitrary[($className$Page.type, JsValue)] ::";\ - next }1' ../test/generators/UserDataGenerator.scala > tmp && mv tmp ../test/generators/UserDataGenerator.scala + next }1' ../test/generators/UserAnswersGenerator.scala > tmp && mv tmp ../test/generators/UserAnswersGenerator.scala echo "Adding helper method to CheckYourAnswersHelper" awk '/class/ {\ diff --git a/src/main/scaffolds/optionsPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/optionsPage/app/controllers/$className$Controller.scala index dac73654..29b608ad 100644 --- a/src/main/scaffolds/optionsPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/optionsPage/app/controllers/$className$Controller.scala @@ -48,12 +48,10 @@ class $className$Controller @Inject()( Future.successful(BadRequest(view(formWithErrors, mode))), value => { - val updatedAnswers = request.userAnswers.set($className$Page, value) - - sessionRepository.set(updatedAnswers.userData).map( - _ => - Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) - ) + for { + updatedAnswers <- Future.fromTry(request.userAnswers.set($className$Page, value)) + _ <- sessionRepository.set(updatedAnswers) + } yield Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) } ) } diff --git a/src/main/scaffolds/optionsPage/app/models/$className$.scala b/src/main/scaffolds/optionsPage/app/models/$className$.scala index 770f5f53..15d45e37 100644 --- a/src/main/scaffolds/optionsPage/app/models/$className$.scala +++ b/src/main/scaffolds/optionsPage/app/models/$className$.scala @@ -5,7 +5,7 @@ import viewmodels.RadioOption sealed trait $className$ -object $className$ { +object $className$ extends Enumerable.Implicits { case object $option1key;format="Camel"$ extends WithName("$option1key;format="decap"$") with $className$ case object $option2key;format="Camel"$ extends WithName("$option2key;format="decap"$") with $className$ @@ -21,16 +21,4 @@ object $className$ { implicit val enumerable: Enumerable[$className$] = Enumerable(values.toSeq.map(v => v.toString -> v): _*) - - implicit object $className$Writes extends Writes[$className$] { - def writes($className;format="decap"$: $className$) = Json.toJson($className;format="decap"$.toString) - } - - implicit object $className$Reads extends Reads[$className$] { - override def reads(json: JsValue): JsResult[$className$] = json match { - case JsString($option1key;format="Camel"$.toString) => JsSuccess($option1key;format="Camel"$) - case JsString($option2key;format="Camel"$.toString) => JsSuccess($option2key;format="Camel"$) - case _ => JsError("Unknown $className;format="decap"$") - } - } } diff --git a/src/main/scaffolds/optionsPage/app/pages/$className$Page.scala b/src/main/scaffolds/optionsPage/app/pages/$className$Page.scala index 81c02228..75047f96 100644 --- a/src/main/scaffolds/optionsPage/app/pages/$className$Page.scala +++ b/src/main/scaffolds/optionsPage/app/pages/$className$Page.scala @@ -1,8 +1,11 @@ package pages import models.$className$ +import play.api.libs.json.JsPath case object $className$Page extends QuestionPage[$className$] { + override def path: JsPath = JsPath \ toString + override def toString: String = "$className;format="decap"$" } diff --git a/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala index e01d7534..9b9e11d4 100644 --- a/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala @@ -2,7 +2,7 @@ package controllers import base.SpecBase import forms.$className$FormProvider -import models.{NormalMode, $className$, UserData} +import models.{NormalMode, $className$, UserAnswers} import navigation.{FakeNavigator, Navigator} import pages.$className$Page import play.api.inject.bind @@ -25,7 +25,7 @@ class $className$ControllerSpec extends SpecBase { "return OK and the correct view for a GET" in { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -41,9 +41,9 @@ class $className$ControllerSpec extends SpecBase { "populate the view correctly on a GET when the question has previously been answered" in { - val userData = UserData(userDataId, Json.obj($className$Page.toString -> JsString($className$.values.head.toString))) + val userAnswers = UserAnswers(userAnswersId).set($className$Page, $className$.values.head).success.value - val application = applicationBuilder(userData = Some(userData)).build() + val application = applicationBuilder(userAnswers = Some(userAnswers)).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -60,7 +60,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to the next page when valid data is submitted" in { val application = - applicationBuilder(userData = Some(emptyUserData)) + applicationBuilder(userAnswers = Some(emptyUserAnswers)) .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) .build() @@ -77,7 +77,7 @@ class $className$ControllerSpec extends SpecBase { "return a Bad Request and errors when invalid data is submitted" in { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val request = FakeRequest(POST, $className;format="decap"$Route) @@ -97,7 +97,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to Session Expired for a GET if no existing data is found" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -109,7 +109,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to Session Expired for a POST if no existing data is found" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val request = FakeRequest(POST, $className;format="decap"$Route) diff --git a/src/main/scaffolds/optionsPage/generated-test/models/$className$Spec.scala b/src/main/scaffolds/optionsPage/generated-test/models/$className$Spec.scala index 077bdefc..0a66378b 100644 --- a/src/main/scaffolds/optionsPage/generated-test/models/$className$Spec.scala +++ b/src/main/scaffolds/optionsPage/generated-test/models/$className$Spec.scala @@ -28,7 +28,7 @@ class $className$Spec extends WordSpec with MustMatchers with PropertyChecks wit forAll(gen) { invalidValue => - JsString(invalidValue).validate[$className$] mustEqual JsError("Unknown $className;format="decap"$") + JsString(invalidValue).validate[$className$] mustEqual JsError("error.invalid") } } diff --git a/src/main/scaffolds/optionsPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/optionsPage/generated-test/views/$className$ViewSpec.scala index 0f4fcf59..53807fe8 100644 --- a/src/main/scaffolds/optionsPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/optionsPage/generated-test/views/$className$ViewSpec.scala @@ -13,7 +13,7 @@ class $className$ViewSpec extends ViewBehaviours { val form = new $className$FormProvider()() - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val view = application.injector.instanceOf[$className$View] diff --git a/src/main/scaffolds/optionsPage/migrations/$className__snake$.sh b/src/main/scaffolds/optionsPage/migrations/$className__snake$.sh index 99d9b072..d62a3643 100644 --- a/src/main/scaffolds/optionsPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/optionsPage/migrations/$className__snake$.sh @@ -52,11 +52,11 @@ awk '/trait ModelGenerators/ {\ print " }";\ next }1' ../test/generators/ModelGenerators.scala > tmp && mv tmp ../test/generators/ModelGenerators.scala -echo "Adding to UserDataGenerator" +echo "Adding to UserAnswersGenerator" awk '/val generators/ {\ print;\ print " arbitrary[($className$Page.type, JsValue)] ::";\ - next }1' ../test/generators/UserDataGenerator.scala > tmp && mv tmp ../test/generators/UserDataGenerator.scala + next }1' ../test/generators/UserAnswersGenerator.scala > tmp && mv tmp ../test/generators/UserAnswersGenerator.scala echo "Adding helper method to CheckYourAnswersHelper" awk '/class/ {\ diff --git a/src/main/scaffolds/page/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/page/generated-test/controllers/$className$ControllerSpec.scala index a8938437..07ee181b 100644 --- a/src/main/scaffolds/page/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/page/generated-test/controllers/$className$ControllerSpec.scala @@ -11,7 +11,7 @@ class $className$ControllerSpec extends SpecBase { "return OK and the correct view for a GET" in { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val request = FakeRequest(GET, routes.$className$Controller.onPageLoad().url) diff --git a/src/main/scaffolds/page/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/page/generated-test/views/$className$ViewSpec.scala index 6cc8f53e..37dc6d4c 100644 --- a/src/main/scaffolds/page/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/page/generated-test/views/$className$ViewSpec.scala @@ -7,7 +7,7 @@ class $className$ViewSpec extends ViewBehaviours { "$className$ view" must { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val view = application.injector.instanceOf[$className$View] diff --git a/src/main/scaffolds/questionPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/questionPage/app/controllers/$className$Controller.scala index 4897ae12..1bf552a0 100644 --- a/src/main/scaffolds/questionPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/questionPage/app/controllers/$className$Controller.scala @@ -48,12 +48,10 @@ class $className$Controller @Inject()( Future.successful(BadRequest(view(formWithErrors, mode))), value => { - val updatedAnswers = request.userAnswers.set($className$Page, value) - - sessionRepository.set(updatedAnswers.userData).map( - _ => - Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) - ) + for { + updatedAnswers <- Future.fromTry(request.userAnswers.set($className$Page, value)) + _ <- sessionRepository.set(updatedAnswers) + } yield Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) } ) } diff --git a/src/main/scaffolds/questionPage/app/pages/$className$Page.scala b/src/main/scaffolds/questionPage/app/pages/$className$Page.scala index 81c02228..75047f96 100644 --- a/src/main/scaffolds/questionPage/app/pages/$className$Page.scala +++ b/src/main/scaffolds/questionPage/app/pages/$className$Page.scala @@ -1,8 +1,11 @@ package pages import models.$className$ +import play.api.libs.json.JsPath case object $className$Page extends QuestionPage[$className$] { + override def path: JsPath = JsPath \ toString + override def toString: String = "$className;format="decap"$" } diff --git a/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala index 90877c51..ccf1eb89 100644 --- a/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala @@ -2,7 +2,7 @@ package controllers import base.SpecBase import forms.$className$FormProvider -import models.{NormalMode, $className$, UserData} +import models.{NormalMode, $className$, UserAnswers} import navigation.{FakeNavigator, Navigator} import pages.$className$Page import play.api.inject.bind @@ -21,8 +21,8 @@ class $className$ControllerSpec extends SpecBase { lazy val $className;format="decap"$Route = routes.$className$Controller.onPageLoad(NormalMode).url - val userData = UserData( - userDataId, + val userAnswers = UserAnswers( + userAnswersId, Json.obj( $className$Page.toString -> Json.obj( "field1" -> "value 1", @@ -35,7 +35,7 @@ class $className$ControllerSpec extends SpecBase { "return OK and the correct view for a GET" in { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -51,7 +51,7 @@ class $className$ControllerSpec extends SpecBase { "populate the view correctly on a GET when the question has previously been answered" in { - val application = applicationBuilder(userData = Some(userData)).build() + val application = applicationBuilder(userAnswers = Some(userAnswers)).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -68,7 +68,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to the next page when valid data is submitted" in { val application = - applicationBuilder(userData = Some(emptyUserData)) + applicationBuilder(userAnswers = Some(emptyUserAnswers)) .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) .build() @@ -85,7 +85,7 @@ class $className$ControllerSpec extends SpecBase { "return a Bad Request and errors when invalid data is submitted" in { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val request = FakeRequest(POST, $className;format="decap"$Route) @@ -105,7 +105,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to Session Expired for a GET if no existing data is found" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -117,7 +117,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to Session Expired for a POST if no existing data is found" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val request = FakeRequest(POST, $className;format="decap"$Route) diff --git a/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala index 8f25f26d..5fa4598c 100644 --- a/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala @@ -16,7 +16,7 @@ class $className$ViewSpec extends QuestionViewBehaviours[$className$] { "$className$View" must { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val view = application.injector.instanceOf[$className$View] diff --git a/src/main/scaffolds/questionPage/migrations/$className__snake$.sh b/src/main/scaffolds/questionPage/migrations/$className__snake$.sh index e5a92b72..f1248b6d 100644 --- a/src/main/scaffolds/questionPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/questionPage/migrations/$className__snake$.sh @@ -58,11 +58,11 @@ awk '/trait ModelGenerators/ {\ print " }";\ next }1' ../test/generators/ModelGenerators.scala > tmp && mv tmp ../test/generators/ModelGenerators.scala -echo "Adding to UserDataGenerator" +echo "Adding to UserAnswersGenerator" awk '/val generators/ {\ print;\ print " arbitrary[($className$Page.type, JsValue)] ::";\ - next }1' ../test/generators/UserDataGenerator.scala > tmp && mv tmp ../test/generators/UserDataGenerator.scala + next }1' ../test/generators/UserAnswersGenerator.scala > tmp && mv tmp ../test/generators/UserAnswersGenerator.scala echo "Adding helper method to CheckYourAnswersHelper" awk '/class/ {\ diff --git a/src/main/scaffolds/stringPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/stringPage/app/controllers/$className$Controller.scala index f70aacb6..d3b14ba5 100644 --- a/src/main/scaffolds/stringPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/stringPage/app/controllers/$className$Controller.scala @@ -48,12 +48,10 @@ class $className$Controller @Inject()( Future.successful(BadRequest(view(formWithErrors, mode))), value => { - val updatedAnswers = request.userAnswers.set($className$Page, value) - - sessionRepository.set(updatedAnswers.userData).map( - _ => - Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) - ) + for { + updatedAnswers <- Future.fromTry(request.userAnswers.set($className$Page, value)) + _ <- sessionRepository.set(updatedAnswers) + } yield Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) } ) } diff --git a/src/main/scaffolds/stringPage/app/pages/$className$Page.scala b/src/main/scaffolds/stringPage/app/pages/$className$Page.scala index c79c3f36..9fec7166 100644 --- a/src/main/scaffolds/stringPage/app/pages/$className$Page.scala +++ b/src/main/scaffolds/stringPage/app/pages/$className$Page.scala @@ -1,6 +1,10 @@ package pages +import play.api.libs.json.JsPath + case object $className$Page extends QuestionPage[String] { + override def path: JsPath = JsPath \ toString + override def toString: String = "$className;format="decap"$" } diff --git a/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala index 9e89311a..95dafd69 100644 --- a/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala @@ -2,7 +2,7 @@ package controllers import base.SpecBase import forms.$className$FormProvider -import models.{NormalMode, UserData} +import models.{NormalMode, UserAnswers} import navigation.{FakeNavigator, Navigator} import pages.$className$Page import play.api.inject.bind @@ -25,7 +25,7 @@ class $className$ControllerSpec extends SpecBase { "return OK and the correct view for a GET" in { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -41,9 +41,9 @@ class $className$ControllerSpec extends SpecBase { "populate the view correctly on a GET when the question has previously been answered" in { - val userData = UserData(userDataId, Json.obj($className$Page.toString -> JsString("answer"))) + val userAnswers = UserAnswers(userAnswersId).set($className$Page, "answer").success.value - val application = applicationBuilder(userData = Some(userData)).build() + val application = applicationBuilder(userAnswers = Some(userAnswers)).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -60,7 +60,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to the next page when valid data is submitted" in { val application = - applicationBuilder(userData = Some(emptyUserData)) + applicationBuilder(userAnswers = Some(emptyUserAnswers)) .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) .build() @@ -76,7 +76,7 @@ class $className$ControllerSpec extends SpecBase { "return a Bad Request and errors when invalid data is submitted" in { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val request = FakeRequest(POST, $className;format="decap"$Route) @@ -96,7 +96,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to Session Expired for a GET if no existing data is found" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -109,7 +109,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to Session Expired for a POST if no existing data is found" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val request = FakeRequest(POST, $className;format="decap"$Route) diff --git a/src/main/scaffolds/stringPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/stringPage/generated-test/views/$className$ViewSpec.scala index e7098da8..be85e19f 100644 --- a/src/main/scaffolds/stringPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/stringPage/generated-test/views/$className$ViewSpec.scala @@ -16,7 +16,7 @@ class $className$ViewSpec extends StringViewBehaviours { "$className$View view" must { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val view = application.injector.instanceOf[$className$View] diff --git a/src/main/scaffolds/stringPage/migrations/$className__snake$.sh b/src/main/scaffolds/stringPage/migrations/$className__snake$.sh index b71e938f..c5e777ee 100644 --- a/src/main/scaffolds/stringPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/stringPage/migrations/$className__snake$.sh @@ -41,11 +41,11 @@ awk '/trait PageGenerators/ {\ print " Arbitrary($className$Page)";\ next }1' ../test/generators/PageGenerators.scala > tmp && mv tmp ../test/generators/PageGenerators.scala -echo "Adding to UserDataGenerator" +echo "Adding to UserAnswersGenerator" awk '/val generators/ {\ print;\ print " arbitrary[($className$Page.type, JsValue)] ::";\ - next }1' ../test/generators/UserDataGenerator.scala > tmp && mv tmp ../test/generators/UserDataGenerator.scala + next }1' ../test/generators/UserAnswersGenerator.scala > tmp && mv tmp ../test/generators/UserAnswersGenerator.scala echo "Adding helper method to CheckYourAnswersHelper" awk '/class/ {\ diff --git a/src/main/scaffolds/yesNoPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/yesNoPage/app/controllers/$className$Controller.scala index 5ef1695f..7d9ea03b 100644 --- a/src/main/scaffolds/yesNoPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/yesNoPage/app/controllers/$className$Controller.scala @@ -48,12 +48,10 @@ class $className;format="cap"$Controller @Inject()( Future.successful(BadRequest(view(formWithErrors, mode))), value => { - val updatedAnswers = request.userAnswers.set($className$Page, value) - - sessionRepository.set(updatedAnswers.userData).map( - _ => - Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) - ) + for { + updatedAnswers <- Future.fromTry(request.userAnswers.set($className$Page, value)) + _ <- sessionRepository.set(updatedAnswers) + } yield Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) } ) } diff --git a/src/main/scaffolds/yesNoPage/app/pages/$className$Page.scala b/src/main/scaffolds/yesNoPage/app/pages/$className$Page.scala index 71810885..bc62b5af 100644 --- a/src/main/scaffolds/yesNoPage/app/pages/$className$Page.scala +++ b/src/main/scaffolds/yesNoPage/app/pages/$className$Page.scala @@ -1,6 +1,10 @@ package pages +import play.api.libs.json.JsPath + case object $className$Page extends QuestionPage[Boolean] { + override def path: JsPath = JsPath \ toString + override def toString: String = "$className;format="decap"$" } diff --git a/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala index 9edbcec6..f8d77b4c 100644 --- a/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala @@ -2,7 +2,7 @@ package controllers import base.SpecBase import forms.$className$FormProvider -import models.{NormalMode, UserData} +import models.{NormalMode, UserAnswers} import navigation.{FakeNavigator, Navigator} import pages.$className$Page import play.api.inject.bind @@ -25,7 +25,7 @@ class $className$ControllerSpec extends SpecBase { "return OK and the correct view for a GET" in { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -41,9 +41,9 @@ class $className$ControllerSpec extends SpecBase { "populate the view correctly on a GET when the question has previously been answered" in { - val userData = UserData(userDataId, Json.obj($className$Page.toString -> JsBoolean(true))) + val userAnswers = UserAnswers(userAnswersId).set($className$Page, true).success.value - val application = applicationBuilder(userData = Some(userData)).build() + val application = applicationBuilder(userAnswers = Some(userAnswers)).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -60,7 +60,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to the next page when valid data is submitted" in { val application = - applicationBuilder(userData = Some(emptyUserData)) + applicationBuilder(userAnswers = Some(emptyUserAnswers)) .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) .build() @@ -77,7 +77,7 @@ class $className$ControllerSpec extends SpecBase { "return a Bad Request and errors when invalid data is submitted" in { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val request = FakeRequest(POST, $className;format="decap"$Route) @@ -97,7 +97,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to Session Expired for a GET if no existing data is found" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val request = FakeRequest(GET, $className;format="decap"$Route) @@ -110,7 +110,7 @@ class $className$ControllerSpec extends SpecBase { "redirect to Session Expired for a POST if no existing data is found" in { - val application = applicationBuilder(userData = None).build() + val application = applicationBuilder(userAnswers = None).build() val request = FakeRequest(POST, $className;format="decap"$Route) diff --git a/src/main/scaffolds/yesNoPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/yesNoPage/generated-test/views/$className$ViewSpec.scala index ba17da50..fc307eef 100644 --- a/src/main/scaffolds/yesNoPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/yesNoPage/generated-test/views/$className$ViewSpec.scala @@ -16,7 +16,7 @@ class $className$ViewSpec extends YesNoViewBehaviours { "$className$ view" must { - val application = applicationBuilder(userData = Some(emptyUserData)).build() + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() val view = application.injector.instanceOf[$className$View] diff --git a/src/main/scaffolds/yesNoPage/migrations/$className__snake$.sh b/src/main/scaffolds/yesNoPage/migrations/$className__snake$.sh index c0f6dae3..338d0672 100644 --- a/src/main/scaffolds/yesNoPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/yesNoPage/migrations/$className__snake$.sh @@ -40,11 +40,11 @@ awk '/trait PageGenerators/ {\ print " Arbitrary($className$Page)";\ next }1' ../test/generators/PageGenerators.scala > tmp && mv tmp ../test/generators/PageGenerators.scala -echo "Adding to UserDataGenerator" +echo "Adding to UserAnswersGenerator" awk '/val generators/ {\ print;\ print " arbitrary[($className$Page.type, JsValue)] ::";\ - next }1' ../test/generators/UserDataGenerator.scala > tmp && mv tmp ../test/generators/UserDataGenerator.scala + next }1' ../test/generators/UserAnswersGenerator.scala > tmp && mv tmp ../test/generators/UserAnswersGenerator.scala echo "Adding helper method to CheckYourAnswersHelper" awk '/class/ {\ From e2a0866250cad6c8ed08aceeca89e8e86d5f70d7 Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Thu, 25 Apr 2019 09:30:19 +0100 Subject: [PATCH 04/23] Ensure application is stopped when running view tests --- src/main/g8/test/base/SpecBase.scala | 5 +++-- src/main/g8/test/views/ViewSpecBase.scala | 10 ++++++++++ .../generated-test/views/$className$ViewSpec.scala | 4 +--- .../generated-test/views/$className$ViewSpec.scala | 4 +--- .../generated-test/views/$className$ViewSpec.scala | 4 +--- .../generated-test/views/$className$ViewSpec.scala | 4 +--- .../generated-test/views/$className$ViewSpec.scala | 4 +--- .../generated-test/views/$className$ViewSpec.scala | 4 +--- 8 files changed, 19 insertions(+), 20 deletions(-) diff --git a/src/main/g8/test/base/SpecBase.scala b/src/main/g8/test/base/SpecBase.scala index 3e4ceee2..0b23dded 100644 --- a/src/main/g8/test/base/SpecBase.scala +++ b/src/main/g8/test/base/SpecBase.scala @@ -4,6 +4,7 @@ import config.FrontendAppConfig import controllers.actions._ import models.UserAnswers import org.scalatest.TryValues +import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures} import org.scalatestplus.play.PlaySpec import org.scalatestplus.play.guice._ import play.api.i18n.{Messages, MessagesApi} @@ -12,7 +13,7 @@ import play.api.inject.{Injector, bind} import play.api.libs.json.Json import play.api.test.FakeRequest -trait SpecBase extends PlaySpec with GuiceOneAppPerSuite with TryValues { +trait SpecBase extends PlaySpec with GuiceOneAppPerSuite with TryValues with ScalaFutures with IntegrationPatience { val userAnswersId = "id" @@ -26,7 +27,7 @@ trait SpecBase extends PlaySpec with GuiceOneAppPerSuite with TryValues { def fakeRequest = FakeRequest("", "") - def messages: Messages = messagesApi.preferred(fakeRequest) + implicit def messages: Messages = messagesApi.preferred(fakeRequest) protected def applicationBuilder(userAnswers: Option[UserAnswers] = None): GuiceApplicationBuilder = new GuiceApplicationBuilder() diff --git a/src/main/g8/test/views/ViewSpecBase.scala b/src/main/g8/test/views/ViewSpecBase.scala index df041088..23e9c2bc 100644 --- a/src/main/g8/test/views/ViewSpecBase.scala +++ b/src/main/g8/test/views/ViewSpecBase.scala @@ -1,12 +1,22 @@ package views +import models.UserAnswers import org.jsoup.Jsoup import org.jsoup.nodes.Document import play.twirl.api.Html import base.SpecBase +import scala.reflect.ClassTag + trait ViewSpecBase extends SpecBase { + def viewFor[A](data: Option[UserAnswers] = None)(implicit tag: ClassTag[A]): A = { + val application = applicationBuilder(data).build() + val view = application.injector.instanceOf[A] + application.stop() + view + } + def asDocument(html: Html): Document = Jsoup.parse(html.toString()) def assertEqualsMessage(doc: Document, cssSelector: String, expectedMessageKey: String) = diff --git a/src/main/scaffolds/intPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/intPage/generated-test/views/$className$ViewSpec.scala index 5d3f28c5..da59b8fd 100644 --- a/src/main/scaffolds/intPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/intPage/generated-test/views/$className$ViewSpec.scala @@ -16,9 +16,7 @@ class $className$ViewSpec extends IntViewBehaviours { "$className$View view" must { - val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() - - val view = application.injector.instanceOf[$className$View] + val view = viewFor[$className$View](Some(emptyUserAnswers)) def applyView(form: Form[_]): HtmlFormat.Appendable = view.apply(form, NormalMode)(fakeRequest, messages) diff --git a/src/main/scaffolds/optionsPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/optionsPage/generated-test/views/$className$ViewSpec.scala index 53807fe8..277bf675 100644 --- a/src/main/scaffolds/optionsPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/optionsPage/generated-test/views/$className$ViewSpec.scala @@ -13,9 +13,7 @@ class $className$ViewSpec extends ViewBehaviours { val form = new $className$FormProvider()() - val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() - - val view = application.injector.instanceOf[$className$View] + val view = viewFor[$className$View](Some(emptyUserAnswers)) def applyView(form: Form[_]): HtmlFormat.Appendable = view.apply(form, NormalMode)(fakeRequest, messages) diff --git a/src/main/scaffolds/page/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/page/generated-test/views/$className$ViewSpec.scala index 37dc6d4c..fdb43a57 100644 --- a/src/main/scaffolds/page/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/page/generated-test/views/$className$ViewSpec.scala @@ -7,9 +7,7 @@ class $className$ViewSpec extends ViewBehaviours { "$className$ view" must { - val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() - - val view = application.injector.instanceOf[$className$View] + val view = viewFor[$className$View](Some(emptyUserAnswers)) val applyView = view.apply()(fakeRequest, messages) diff --git a/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala index 5fa4598c..6d669a8e 100644 --- a/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala @@ -16,9 +16,7 @@ class $className$ViewSpec extends QuestionViewBehaviours[$className$] { "$className$View" must { - val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() - - val view = application.injector.instanceOf[$className$View] + val view = viewFor[$className$View](Some(emptyUserAnswers)) def applyView(form: Form[_]): HtmlFormat.Appendable = view.apply(form, NormalMode)(fakeRequest, messages) diff --git a/src/main/scaffolds/stringPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/stringPage/generated-test/views/$className$ViewSpec.scala index be85e19f..447689b6 100644 --- a/src/main/scaffolds/stringPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/stringPage/generated-test/views/$className$ViewSpec.scala @@ -16,9 +16,7 @@ class $className$ViewSpec extends StringViewBehaviours { "$className$View view" must { - val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() - - val view = application.injector.instanceOf[$className$View] + val view = viewFor[$className$View](Some(emptyUserAnswers)) def applyView(form: Form[_]): HtmlFormat.Appendable = view.apply(form, NormalMode)(fakeRequest, messages) diff --git a/src/main/scaffolds/yesNoPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/yesNoPage/generated-test/views/$className$ViewSpec.scala index fc307eef..48870b75 100644 --- a/src/main/scaffolds/yesNoPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/yesNoPage/generated-test/views/$className$ViewSpec.scala @@ -16,9 +16,7 @@ class $className$ViewSpec extends YesNoViewBehaviours { "$className$ view" must { - val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() - - val view = application.injector.instanceOf[$className$View] + val view = viewFor[$className$View](Some(emptyUserAnswers)) def applyView(form: Form[_]): HtmlFormat.Appendable = view.apply(form, NormalMode)(fakeRequest, messages) From 72258ff2d826e203cdbf414a938f58efc7cbbdb9 Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Thu, 25 Apr 2019 10:43:39 +0100 Subject: [PATCH 05/23] Fix controller tests * Ensure application is stopped in every test * Mock out session repository in tests which interact with it --- .../$className$ControllerSpec.scala | 29 ++++++++++++++++-- .../$className$ControllerSpec.scala | 29 ++++++++++++++++-- .../$className$ControllerSpec.scala | 2 ++ .../$className$ControllerSpec.scala | 30 +++++++++++++++++-- .../$className$ControllerSpec.scala | 29 ++++++++++++++++-- .../$className$ControllerSpec.scala | 29 ++++++++++++++++-- 6 files changed, 138 insertions(+), 10 deletions(-) diff --git a/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala index 96b91100..b1a361bc 100644 --- a/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala @@ -4,15 +4,21 @@ import base.SpecBase import forms.$className$FormProvider import models.{NormalMode, UserAnswers} import navigation.{FakeNavigator, Navigator} +import org.mockito.Matchers.any +import org.mockito.Mockito.when +import org.scalatest.mockito.MockitoSugar import pages.$className$Page import play.api.inject.bind import play.api.libs.json.{JsNumber, Json} import play.api.mvc.Call import play.api.test.FakeRequest import play.api.test.Helpers._ +import repositories.SessionRepository import views.html.$className$View -class $className$ControllerSpec extends SpecBase { +import scala.concurrent.Future + +class $className$ControllerSpec extends SpecBase with MockitoSugar { val formProvider = new $className$FormProvider() val form = formProvider() @@ -39,6 +45,8 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(form, NormalMode)(fakeRequest, messages).toString + + application.stop() } "populate the view correctly on a GET when the question has previously been answered" in { @@ -57,13 +65,22 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(form.fill(validAnswer), NormalMode)(fakeRequest, messages).toString + + application.stop() } "redirect to the next page when valid data is submitted" in { + val mockSessionRepository = mock[SessionRepository] + + when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) - .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) + .overrides( + bind[Navigator].toInstance(new FakeNavigator(onwardRoute)), + bind[SessionRepository].toInstance(mockSessionRepository) + ) .build() val request = @@ -75,6 +92,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual onwardRoute.url + + application.stop() } "return a Bad Request and errors when invalid data is submitted" in { @@ -95,6 +114,8 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(boundForm, NormalMode)(fakeRequest, messages).toString + + application.stop() } "redirect to Session Expired for a GET if no existing data is found" in { @@ -107,6 +128,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() } "redirect to Session Expired for a POST if no existing data is found" in { @@ -122,6 +145,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() } } } diff --git a/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala index 9b9e11d4..4df41864 100644 --- a/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala @@ -4,15 +4,21 @@ import base.SpecBase import forms.$className$FormProvider import models.{NormalMode, $className$, UserAnswers} import navigation.{FakeNavigator, Navigator} +import org.mockito.Matchers.any +import org.mockito.Mockito.when +import org.scalatest.mockito.MockitoSugar import pages.$className$Page import play.api.inject.bind import play.api.libs.json.{JsString, Json} import play.api.mvc.Call import play.api.test.FakeRequest import play.api.test.Helpers._ +import repositories.SessionRepository import views.html.$className$View -class $className$ControllerSpec extends SpecBase { +import scala.concurrent.Future + +class $className$ControllerSpec extends SpecBase with MockitoSugar { def onwardRoute = Call("GET", "/foo") @@ -37,6 +43,8 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(form, NormalMode)(fakeRequest, messages).toString + + application.stop() } "populate the view correctly on a GET when the question has previously been answered" in { @@ -55,13 +63,22 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(form.fill($className$.values.head), NormalMode)(fakeRequest, messages).toString + + application.stop() } "redirect to the next page when valid data is submitted" in { + val mockSessionRepository = mock[SessionRepository] + + when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) - .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) + .overrides( + bind[Navigator].toInstance(new FakeNavigator(onwardRoute)), + bind[SessionRepository].toInstance(mockSessionRepository) + ) .build() val request = @@ -73,6 +90,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual onwardRoute.url + + application.stop() } "return a Bad Request and errors when invalid data is submitted" in { @@ -93,6 +112,8 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(boundForm, NormalMode)(fakeRequest, messages).toString + + application.stop() } "redirect to Session Expired for a GET if no existing data is found" in { @@ -105,6 +126,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() } "redirect to Session Expired for a POST if no existing data is found" in { @@ -120,6 +143,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() } } } diff --git a/src/main/scaffolds/page/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/page/generated-test/controllers/$className$ControllerSpec.scala index 07ee181b..b13c8610 100644 --- a/src/main/scaffolds/page/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/page/generated-test/controllers/$className$ControllerSpec.scala @@ -23,6 +23,8 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view()(fakeRequest, messages).toString + + application.stop() } } } diff --git a/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala index ccf1eb89..74e7cfc5 100644 --- a/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala @@ -4,15 +4,21 @@ import base.SpecBase import forms.$className$FormProvider import models.{NormalMode, $className$, UserAnswers} import navigation.{FakeNavigator, Navigator} +import org.mockito.Matchers.any +import org.mockito.Mockito.when +import org.scalatest.mockito.MockitoSugar import pages.$className$Page import play.api.inject.bind import play.api.libs.json.Json import play.api.mvc.Call import play.api.test.FakeRequest import play.api.test.Helpers._ +import repositories.SessionRepository import views.html.$className$View -class $className$ControllerSpec extends SpecBase { +import scala.concurrent.Future + +class $className$ControllerSpec extends SpecBase with MockitoSugar { def onwardRoute = Call("GET", "/foo") @@ -47,6 +53,8 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(form, NormalMode)(request, messages).toString + + application.stop() } "populate the view correctly on a GET when the question has previously been answered" in { @@ -63,15 +71,25 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(form.fill($className$("value 1", "value 2")), NormalMode)(fakeRequest, messages).toString + + application.stop() } "redirect to the next page when valid data is submitted" in { + val mockSessionRepository = mock[SessionRepository] + + when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) - .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) + .overrides( + bind[Navigator].toInstance(new FakeNavigator(onwardRoute)), + bind[SessionRepository].toInstance(mockSessionRepository) + ) .build() + val request = FakeRequest(POST, $className;format="decap"$Route) .withFormUrlEncodedBody(("field1", "value 1"), ("field2", "value 2")) @@ -81,6 +99,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual onwardRoute.url + + application.stop() } "return a Bad Request and errors when invalid data is submitted" in { @@ -101,6 +121,8 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(boundForm, NormalMode)(fakeRequest, messages).toString + + application.stop() } "redirect to Session Expired for a GET if no existing data is found" in { @@ -113,6 +135,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() } "redirect to Session Expired for a POST if no existing data is found" in { @@ -128,6 +152,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() } } } diff --git a/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala index 95dafd69..6904fbb1 100644 --- a/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala @@ -4,15 +4,21 @@ import base.SpecBase import forms.$className$FormProvider import models.{NormalMode, UserAnswers} import navigation.{FakeNavigator, Navigator} +import org.mockito.Matchers.any +import org.mockito.Mockito.when +import org.scalatest.mockito.MockitoSugar import pages.$className$Page import play.api.inject.bind import play.api.libs.json.{JsString, Json} import play.api.mvc.Call import play.api.test.FakeRequest import play.api.test.Helpers._ +import repositories.SessionRepository import views.html.$className$View -class $className$ControllerSpec extends SpecBase { +import scala.concurrent.Future + +class $className$ControllerSpec extends SpecBase with MockitoSugar { def onwardRoute = Call("GET", "/foo") @@ -37,6 +43,8 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(form, NormalMode)(fakeRequest, messages).toString + + application.stop() } "populate the view correctly on a GET when the question has previously been answered" in { @@ -55,13 +63,22 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(form.fill("answer"), NormalMode)(fakeRequest, messages).toString + + application.stop() } "redirect to the next page when valid data is submitted" in { + val mockSessionRepository = mock[SessionRepository] + + when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) - .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) + .overrides( + bind[Navigator].toInstance(new FakeNavigator(onwardRoute)), + bind[SessionRepository].toInstance(mockSessionRepository) + ) .build() val request = @@ -72,6 +89,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual onwardRoute.url + + application.stop() } "return a Bad Request and errors when invalid data is submitted" in { @@ -92,6 +111,8 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(boundForm, NormalMode)(fakeRequest, messages).toString + + application.stop() } "redirect to Session Expired for a GET if no existing data is found" in { @@ -105,6 +126,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() } "redirect to Session Expired for a POST if no existing data is found" in { @@ -120,6 +143,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() } } } diff --git a/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala index f8d77b4c..a77feea9 100644 --- a/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala @@ -4,15 +4,21 @@ import base.SpecBase import forms.$className$FormProvider import models.{NormalMode, UserAnswers} import navigation.{FakeNavigator, Navigator} +import org.mockito.Matchers.any +import org.mockito.Mockito.when +import org.scalatest.mockito.MockitoSugar import pages.$className$Page import play.api.inject.bind import play.api.libs.json.{JsBoolean, Json} import play.api.mvc.Call import play.api.test.FakeRequest import play.api.test.Helpers._ +import repositories.SessionRepository import views.html.$className$View -class $className$ControllerSpec extends SpecBase { +import scala.concurrent.Future + +class $className$ControllerSpec extends SpecBase with MockitoSugar { def onwardRoute = Call("GET", "/foo") @@ -37,6 +43,8 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(form, NormalMode)(fakeRequest, messages).toString + + application.stop() } "populate the view correctly on a GET when the question has previously been answered" in { @@ -55,13 +63,22 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(form.fill(true), NormalMode)(fakeRequest, messages).toString + + application.stop() } "redirect to the next page when valid data is submitted" in { + val mockSessionRepository = mock[SessionRepository] + + when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)) - .overrides(bind[Navigator].toInstance(new FakeNavigator(onwardRoute))) + .overrides( + bind[Navigator].toInstance(new FakeNavigator(onwardRoute)), + bind[SessionRepository].toInstance(mockSessionRepository) + ) .build() val request = @@ -73,6 +90,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual onwardRoute.url + + application.stop() } "return a Bad Request and errors when invalid data is submitted" in { @@ -93,6 +112,8 @@ class $className$ControllerSpec extends SpecBase { contentAsString(result) mustEqual view(boundForm, NormalMode)(fakeRequest, messages).toString + + application.stop() } "redirect to Session Expired for a GET if no existing data is found" in { @@ -106,6 +127,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() } "redirect to Session Expired for a POST if no existing data is found" in { @@ -121,6 +144,8 @@ class $className$ControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() } } } From ff568b587054d2c87009ee94fcfdd630a8a84808 Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Thu, 25 Apr 2019 11:20:22 +0100 Subject: [PATCH 06/23] Improve RichJsValue --- src/main/g8/app/models/package.scala | 50 ++- src/main/g8/test/models/RichJsValueSpec.scala | 361 ++++++++++++------ 2 files changed, 301 insertions(+), 110 deletions(-) diff --git a/src/main/g8/app/models/package.scala b/src/main/g8/app/models/package.scala index 7cea9a80..d6db1555 100644 --- a/src/main/g8/app/models/package.scala +++ b/src/main/g8/app/models/package.scala @@ -6,6 +6,9 @@ package object models { def setObject(path: JsPath, value: JsValue): JsResult[JsObject] = jsObject.set(path, value).flatMap(_.validate[JsObject]) + + def removeObject(path: JsPath): JsResult[JsObject] = + jsObject.remove(path).flatMap(_.validate[JsObject]) } implicit class RichJsValue(jsValue: JsValue) { @@ -50,7 +53,7 @@ package object models { private def setIndexNode(node: IdxPathNode, oldValue: JsValue, newValue: JsValue): JsResult[JsValue] = { - val index = node.idx + val index: Int = node.idx oldValue match { case oldValue: JsArray if index >= 0 && index <= oldValue.value.length => @@ -66,6 +69,18 @@ package object models { } } + private def removeIndexNode(node: IdxPathNode, valueToRemoveFrom: JsArray): JsResult[JsValue] = { + val index: Int = node.idx + + valueToRemoveFrom match { + case valueToRemoveFrom: JsArray if index >= 0 && index < valueToRemoveFrom.value.length => + val updatedJsArray = valueToRemoveFrom.value.slice(0, index) ++ valueToRemoveFrom.value.slice(index + 1, valueToRemoveFrom.value.size) + JsSuccess(JsArray(updatedJsArray)) + case valueToRemoveFrom: JsArray => JsError(s"array index out of bounds: \$index, \$valueToRemoveFrom") + case _ => JsError(s"cannot set an index on \$valueToRemoveFrom") + } + } + private def setKeyNode(node: KeyPathNode, oldValue: JsValue, newValue: JsValue): JsResult[JsValue] = { val key = node.key @@ -77,5 +92,38 @@ package object models { JsError(s"cannot set a key on \$oldValue") } } + + def remove(path: JsPath): JsResult[JsValue] = { + + (path.path, jsValue) match { + case (Nil, _) => JsError("path cannot be empty") + case ((n: KeyPathNode) :: Nil, value: JsObject) if value.keys.contains(n.key) => JsSuccess(value - n.key) + case ((n: KeyPathNode) :: Nil, value: JsObject) if !value.keys.contains(n.key) => JsError("cannot find value at path") + case ((n: IdxPathNode) :: Nil, value: JsArray) => removeIndexNode(n, value) + case ((_: KeyPathNode) :: Nil, _) => JsError(s"cannot remove a key on \$jsValue") + case (first :: second :: rest, oldValue) => + + Reads.optionNoError(Reads.at[JsValue](JsPath(first :: Nil))) + .reads(oldValue).flatMap { + opt: Option[JsValue] => + + opt.map(JsSuccess(_)).getOrElse { + second match { + case _: KeyPathNode => + JsSuccess(Json.obj()) + case _: IdxPathNode => + JsSuccess(Json.arr()) + case _: RecursiveSearch => + JsError("recursive search is not supported") + } + }.flatMap { + _.remove(JsPath(second :: rest)).flatMap { + newValue => + oldValue.set(JsPath(first :: Nil), newValue) + } + } + } + } + } } } diff --git a/src/main/g8/test/models/RichJsValueSpec.scala b/src/main/g8/test/models/RichJsValueSpec.scala index f478534d..aa4e922c 100644 --- a/src/main/g8/test/models/RichJsValueSpec.scala +++ b/src/main/g8/test/models/RichJsValueSpec.scala @@ -1,202 +1,345 @@ package models +import generators.ModelGenerators import org.scalacheck.{Gen, Shrink} import org.scalatest.prop.PropertyChecks import org.scalatest.{FreeSpec, MustMatchers, OptionValues} import play.api.libs.json._ -class RichJsValueSpec extends FreeSpec with MustMatchers with PropertyChecks with OptionValues { +class RichJsValueSpec extends FreeSpec with MustMatchers with PropertyChecks with OptionValues with ModelGenerators { - implicit val dontShrink: Shrink[String] = Shrink.shrinkAny + implicit def dontShrink[A]: Shrink[A] = Shrink.shrinkAny + val min = 2 + val max = 10 val nonEmptyAlphaStr: Gen[String] = Gen.alphaStr.suchThat(_.nonEmpty) - "set" - { + def buildJsObj[B](keys: Seq[String], values: Seq[B])(implicit writes: Writes[B]): JsObject = { + keys.zip(values).foldLeft(JsObject.empty) { + case (acc, (key, value)) => acc + (key -> Json.toJson[B](value)) + } + } - "must return an error if the path is empty" in { + "set" - { - val value = Json.obj() + "must return an error if the path is empty" in { - value.set(JsPath, Json.obj()) mustEqual JsError("path cannot be empty") - } + val value = Json.obj() - "must set a value on a JsObject" in { + value.set(JsPath, Json.obj()) mustEqual JsError("path cannot be empty") + } - val gen = for { - originalKey <- nonEmptyAlphaStr - originalValue <- nonEmptyAlphaStr - pathKey <- nonEmptyAlphaStr suchThat (_ != originalKey) - newValue <- nonEmptyAlphaStr - } yield (originalKey, originalValue, pathKey, newValue) + "must set a value on a JsObject" in { - forAll(gen) { - case (originalKey, originalValue, pathKey, newValue) => + val gen = for { + originalKey <- nonEmptyAlphaStr + originalValue <- nonEmptyAlphaStr + pathKey <- nonEmptyAlphaStr suchThat (_ != originalKey) + newValue <- nonEmptyAlphaStr + } yield (originalKey, originalValue, pathKey, newValue) - val value = Json.obj(originalKey -> originalValue) + forAll(gen) { + case (originalKey, originalValue, pathKey, newValue) => - val path = JsPath \ pathKey + val value = Json.obj(originalKey -> originalValue) - value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.obj(originalKey -> originalValue, pathKey -> newValue)) + val path = JsPath \ pathKey + + value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.obj(originalKey -> originalValue, pathKey -> newValue)) + } } - } - "must set a nested value on a JsObject" in { + "must set a nested value on a JsObject" in { - val value = Json.obj( - "foo" -> Json.obj() - ) + val value = Json.obj( + "foo" -> Json.obj() + ) - val path = JsPath \ "foo" \ "bar" + val path = JsPath \ "foo" \ "bar" - value.set(path, JsString("baz")).asOpt.value mustEqual Json.obj( - "foo" -> Json.obj( - "bar" -> "baz" + value.set(path, JsString("baz")).asOpt.value mustEqual Json.obj( + "foo" -> Json.obj( + "bar" -> "baz" + ) ) - ) - } + } - "must add a value to an empty JsArray" in { + "must add a value to an empty JsArray" in { - forAll(nonEmptyAlphaStr) { - newValue => + forAll(nonEmptyAlphaStr) { + newValue => - val value = Json.arr() + val value = Json.arr() - val path = JsPath \ 0 + val path = JsPath \ 0 - value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.arr(newValue)) + value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.arr(newValue)) + } } - } - "must add a value to the end of a JsArray" in { + "must add a value to the end of a JsArray" in { - forAll(nonEmptyAlphaStr, nonEmptyAlphaStr) { - (oldValue, newValue) => + forAll(nonEmptyAlphaStr, nonEmptyAlphaStr) { + (oldValue, newValue) => - val value = Json.arr(oldValue) + val value = Json.arr(oldValue) - val path = JsPath \ 1 + val path = JsPath \ 1 - value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.arr(oldValue, newValue)) + value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.arr(oldValue, newValue)) + } } - } - "must change a value in an existing JsArray" in { + "must change a value in an existing JsArray" in { - forAll(nonEmptyAlphaStr, nonEmptyAlphaStr, nonEmptyAlphaStr) { - (firstValue, secondValue, newValue) => + forAll(nonEmptyAlphaStr, nonEmptyAlphaStr, nonEmptyAlphaStr) { + (firstValue, secondValue, newValue) => - val value = Json.arr(firstValue, secondValue) + val value = Json.arr(firstValue, secondValue) - val path = JsPath \ 0 + val path = JsPath \ 0 - value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.arr(newValue, secondValue)) + value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.arr(newValue, secondValue)) + } } - } - "must set a nested value on a JsArray" in { + "must set a nested value on a JsArray" in { + + val value = Json.arr(Json.arr("foo")) + + val path = JsPath \ 0 \ 0 + + value.set(path, JsString("bar")).asOpt.value mustEqual Json.arr(Json.arr("bar")) + } + + "must change the value of an existing key" in { + + val gen = for { + originalKey <- nonEmptyAlphaStr + originalValue <- nonEmptyAlphaStr + newValue <- nonEmptyAlphaStr + } yield (originalKey, originalValue, newValue) + + forAll(gen) { + case (pathKey, originalValue, newValue) => + + val value = Json.obj(pathKey -> originalValue) + + val path = JsPath \ pathKey + + value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.obj(pathKey -> newValue)) + } + } + + "must return an error when trying to set a key on a non-JsObject" in { + + val value = Json.arr() + + val path = JsPath \ "foo" + + value.set(path, JsString("bar")) mustEqual JsError(s"cannot set a key on \$value") + } + + "must return an error when trying to set an index on a non-JsArray" in { + + val value = Json.obj() + + val path = JsPath \ 0 + + value.set(path, JsString("bar")) mustEqual JsError(s"cannot set an index on \$value") + } + + "must return an error when trying to set an index other than zero on an empty array" in { + + val value = Json.arr() + + val path = JsPath \ 1 + + value.set(path, JsString("bar")) mustEqual JsError("array index out of bounds: 1, []") + } + + "must return an error when trying to set an index out of bounds" in { + + val value = Json.arr("bar", "baz") + + val path = JsPath \ 3 + + value.set(path, JsString("fork")) mustEqual JsError("array index out of bounds: 3, [\"bar\",\"baz\"]") + } - val value = Json.arr(Json.arr("foo")) + "must set into an array which does not exist" in { - val path = JsPath \ 0 \ 0 + val value = Json.obj() - value.set(path, JsString("bar")).asOpt.value mustEqual Json.arr(Json.arr("bar")) + val path = JsPath \ "foo" \ 0 + + value.set(path, JsString("bar")) mustEqual JsSuccess(Json.obj( + "foo" -> Json.arr("bar") + )) + } + + "must set into an object which does not exist" in { + + val value = Json.obj() + + val path = JsPath \ "foo" \ "bar" + + value.set(path, JsString("baz")) mustEqual JsSuccess(Json.obj( + "foo" -> Json.obj( + "bar" -> "baz" + ) + )) + } + + "must set nested objects and arrays" in { + + val value = Json.obj() + + val path = JsPath \ "foo" \ 0 \ "bar" \ 0 + + value.set(path, JsString("baz")) mustEqual JsSuccess(Json.obj( + "foo" -> Json.arr( + Json.obj( + "bar" -> Json.arr( + "baz" + ) + ) + ) + )) + } + } + + "remove" - { + "must return an error if the path is empty" in { + + val value = Json.obj() + + value.set(JsPath, Json.obj()) mustEqual JsError("path cannot be empty") } - "must change the value of an existing key" in { + + "must return an error if the path does not contain a value" in { val gen = for { originalKey <- nonEmptyAlphaStr originalValue <- nonEmptyAlphaStr - newValue <- nonEmptyAlphaStr - } yield (originalKey, originalValue, newValue) + pathKey <- nonEmptyAlphaStr suchThat (_ != originalKey) + } yield (originalKey, originalValue, pathKey) forAll(gen) { - case (pathKey, originalValue, newValue) => + case (originalKey, originalValue, pathKey) => - val value = Json.obj(pathKey -> originalValue) + val value = Json.obj(originalKey -> originalValue) val path = JsPath \ pathKey - value.set(path, JsString(newValue)) mustEqual JsSuccess(Json.obj(pathKey -> newValue)) + value.remove(path) mustEqual JsError("cannot find value at path") + } - } - "must return an error when trying to set a key on a non-JsObject" in { + } - val value = Json.arr() + "must remove a value given a keyPathNode and return the new object" in { - val path = JsPath \ "foo" + val gen = for { + keys <- Gen.listOf(nonEmptyAlphaStr) + values <- Gen.listOf(nonEmptyAlphaStr) + keyToRemove <- nonEmptyAlphaStr + valueToRemove <- nonEmptyAlphaStr + } yield (keys, values, keyToRemove, valueToRemove) - value.set(path, JsString("bar")) mustEqual JsError(s"cannot set a key on \$value") - } + forAll(gen) { + case (keys, values, keyToRemove, valueToRemove) => - "must return an error when trying to set an index on a non-JsArray" in { + val initialObj: JsObject = keys.zip(values).foldLeft(JsObject.empty) { + case (acc, (key, value)) => acc + (key -> JsString(value)) + } - val value = Json.obj() + val testObject: JsObject = initialObj + (keyToRemove -> Json.toJson(valueToRemove)) - val path = JsPath \ 0 + val pathToRemove = JsPath \ keyToRemove - value.set(path, JsString("bar")) mustEqual JsError(s"cannot set an index on \$value") + testObject mustNot equal(initialObj) + testObject.remove(pathToRemove) mustEqual JsSuccess(initialObj) + } } - "must return an error when trying to set an index other than zero on an empty array" in { + "must remove a value given an index node and return the new object for one array" in { + + val gen = for { + key <- nonEmptyAlphaStr + values <- Gen.nonEmptyListOf(nonEmptyAlphaStr) + index <- Gen.choose(0, values.size - 1) + } yield (key, values, index) - val value = Json.arr() + forAll(gen) { + case (key: String, values: List[String], indexToRemove: Int) => - val path = JsPath \ 1 + val valuesInArrays: Seq[JsValue] = values.map(Json.toJson[String]) + val initialObj: JsObject = buildJsObj(Seq(key), Seq(valuesInArrays)) - value.set(path, JsString("bar")) mustEqual JsError("array index out of bounds: 1, []") - } - "must return an error when trying to set an index out of bounds" in { + val pathToRemove = JsPath \ key \ indexToRemove - val value = Json.arr("bar", "baz") + val removed: JsResult[JsValue] = initialObj.remove(pathToRemove) - val path = JsPath \ 3 + val expectedOutcome = + buildJsObj( + Seq(key), + Seq(valuesInArrays.slice(0, indexToRemove) ++ valuesInArrays.slice(indexToRemove + 1, values.length) + ) + ) - value.set(path, JsString("fork")) mustEqual JsError("array index out of bounds: 3, [\"bar\",\"baz\"]") + removed mustBe JsSuccess(expectedOutcome) + } } - "must set into an array which does not exist" in { + "remove a value from one of many arrays" in { - val value = Json.obj() + val input = Json.obj( + "key" -> JsArray(Seq(Json.toJson(1), Json.toJson(2))), + "key2" -> JsArray(Seq(Json.toJson(1), Json.toJson(2))) + ) - val path = JsPath \ "foo" \ 0 + val path = JsPath \ "key" \ 0 - value.set(path, JsString("bar")) mustEqual JsSuccess(Json.obj( - "foo" -> Json.arr("bar") - )) + input.remove(path) mustBe JsSuccess( + Json.obj( + "key" -> JsArray(Seq(Json.toJson (2))), "key2" -> JsArray(Seq(Json.toJson(1), Json.toJson(2)))) + ) } + } - "must set into an object which does not exist" in { + "remove a value when there are nested arrays" in { - val value = Json.obj() + val input = Json.obj( + "key" -> JsArray(Seq(JsArray(Seq(Json.toJson(1), Json.toJson(2))), Json.toJson(2))), + "key2" -> JsArray(Seq(Json.toJson(1), Json.toJson(2))) + ) - val path = JsPath \ "foo" \ "bar" + val path = JsPath \ "key" \ 0 \ 0 - value.set(path, JsString("baz")) mustEqual JsSuccess(Json.obj( - "foo" -> Json.obj( - "bar" -> "baz" - ) - )) - } + input.remove(path) mustBe JsSuccess( + Json.obj( + "key" -> JsArray(Seq(JsArray(Seq(Json.toJson(2))), Json.toJson(2))), + "key2" -> JsArray(Seq(Json.toJson(1), Json.toJson(2))) + ) + ) + } - "must set nested objects and arrays" in { + "remove the value if the last value is deleted from an array" in { + val input = Json.obj( + "key" -> JsArray(Seq(Json.toJson(1))), + "key2" -> JsArray(Seq(Json.toJson(1), Json.toJson(2))) + ) - val value = Json.obj() + val path = JsPath \ "key" \ 0 - val path = JsPath \ "foo" \ 0 \ "bar" \ 0 - - value.set(path, JsString("baz")) mustEqual JsSuccess(Json.obj( - "foo" -> Json.arr( - Json.obj( - "bar" -> Json.arr( - "baz" - ) - ) - ) - )) - } + input.remove(path) mustBe JsSuccess( + Json.obj( + "key" -> JsArray(), + "key2" -> JsArray(Seq(Json.toJson(1), Json.toJson(2))) + ) + ) } } From e8b9827e1099a44bc87821f25d48cbac8ef8a890 Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Thu, 25 Apr 2019 11:20:34 +0100 Subject: [PATCH 07/23] Introduce queries --- src/main/g8/app/pages/QuestionPage.scala | 12 ++---------- src/main/g8/app/queries/Query.scala | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 10 deletions(-) create mode 100644 src/main/g8/app/queries/Query.scala diff --git a/src/main/g8/app/pages/QuestionPage.scala b/src/main/g8/app/pages/QuestionPage.scala index 06bc5da3..8d3be4fd 100644 --- a/src/main/g8/app/pages/QuestionPage.scala +++ b/src/main/g8/app/pages/QuestionPage.scala @@ -1,14 +1,6 @@ package pages import models.UserAnswers -import play.api.libs.json.JsPath +import queries.{Gettable, Settable} -import scala.util.{Success, Try} - -trait QuestionPage[A] extends Page { - - def path: JsPath - - def cleanup(value: Option[A], userAnswers: UserAnswers): Try[UserAnswers] = - Success(userAnswers) -} +trait QuestionPage[A] extends Page with Gettable[A] with Settable[A] diff --git a/src/main/g8/app/queries/Query.scala b/src/main/g8/app/queries/Query.scala new file mode 100644 index 00000000..d8a2368c --- /dev/null +++ b/src/main/g8/app/queries/Query.scala @@ -0,0 +1,19 @@ +package queries + +import models.UserAnswers +import play.api.libs.json.JsPath + +import scala.util.{Success, Try} + +sealed trait Query { + + def path: JsPath +} + +trait Gettable[A] extends Query + +trait Settable[A] extends Query { + + def cleanup(value: Option[A], userAnswers: UserAnswers): Try[UserAnswers] = + Success(userAnswers) +} From 606a97e27cfb6987d31ae2fa4b9bc71d815ed44a Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Thu, 25 Apr 2019 13:37:57 +0100 Subject: [PATCH 08/23] Html escape users' answers on CYA pages --- src/main/g8/app/utils/CheckYourAnswersHelper.scala | 13 +++++++++++-- src/main/g8/app/viewmodels/AnswerRow.scala | 4 +++- .../g8/app/views/components/answer_row.scala.html | 12 +++--------- .../intPage/migrations/$className__snake$.sh | 7 ++++++- .../optionsPage/migrations/$className__snake$.sh | 7 ++++++- .../questionPage/migrations/$className__snake$.sh | 7 ++++++- .../stringPage/migrations/$className__snake$.sh | 7 ++++++- .../yesNoPage/migrations/$className__snake$.sh | 8 +++++++- 8 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/main/g8/app/utils/CheckYourAnswersHelper.scala b/src/main/g8/app/utils/CheckYourAnswersHelper.scala index 6c257913..54497fca 100644 --- a/src/main/g8/app/utils/CheckYourAnswersHelper.scala +++ b/src/main/g8/app/utils/CheckYourAnswersHelper.scala @@ -3,7 +3,16 @@ package utils import controllers.routes import models.{CheckMode, UserAnswers} import pages._ -import viewmodels.{AnswerRow, RepeaterAnswerRow, RepeaterAnswerSection} +import play.api.i18n.Messages +import play.twirl.api.{Html, HtmlFormat} +import viewmodels.AnswerRow -class CheckYourAnswersHelper(userAnswers: UserAnswers) { +class CheckYourAnswersHelper(userAnswers: UserAnswers)(implicit messages: Messages) { + + private def yesOrNo(answer: Boolean)(implicit messages: Messages): Html = + if (answer) { + HtmlFormat.escape(messages("site.yes")) + } else { + HtmlFormat.escape(messages("site.no")) + } } diff --git a/src/main/g8/app/viewmodels/AnswerRow.scala b/src/main/g8/app/viewmodels/AnswerRow.scala index f52297a8..c3af194c 100644 --- a/src/main/g8/app/viewmodels/AnswerRow.scala +++ b/src/main/g8/app/viewmodels/AnswerRow.scala @@ -1,3 +1,5 @@ package viewmodels -case class AnswerRow(label: String, answer: String, answerIsMessageKey: Boolean, changeUrl: String) +import play.twirl.api.Html + +case class AnswerRow(label: Html, answer: Html, changeUrl: String) diff --git a/src/main/g8/app/views/components/answer_row.scala.html b/src/main/g8/app/views/components/answer_row.scala.html index 36f460f8..7e17d1dc 100644 --- a/src/main/g8/app/views/components/answer_row.scala.html +++ b/src/main/g8/app/views/components/answer_row.scala.html @@ -3,18 +3,12 @@ @(row: AnswerRow)(implicit messages: Messages)
  • -
    @messages(row.label)
    -
    - @if(row.answerIsMessageKey){ - @messages(row.answer) - } else{ - @row.answer - } -
    +
    @row.label
    +
    @row.answer
  • diff --git a/src/main/scaffolds/intPage/migrations/$className__snake$.sh b/src/main/scaffolds/intPage/migrations/$className__snake$.sh index f37473c5..980aa9ba 100644 --- a/src/main/scaffolds/intPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/intPage/migrations/$className__snake$.sh @@ -54,7 +54,12 @@ awk '/class/ {\ print;\ print "";\ print " def $className;format="decap"$: Option[AnswerRow] = userAnswers.get($className$Page) map {";\ - print " x => AnswerRow(\"$className;format="decap"$.checkYourAnswersLabel\", s\"\$x\", false, routes.$className$Controller.onPageLoad(CheckMode).url)";\ + print " x =>";\ + print " AnswerRow(";\ + print " HtmlFormat.escape(messages(\"$className;format="decap"$.checkYourAnswersLabel\")),";\ + print " HtmlFormat.escape(x.toString),";\ + print " routes.$className$Controller.onPageLoad(CheckMode).url";\ + print " )" print " }";\ next }1' ../app/utils/CheckYourAnswersHelper.scala > tmp && mv tmp ../app/utils/CheckYourAnswersHelper.scala diff --git a/src/main/scaffolds/optionsPage/migrations/$className__snake$.sh b/src/main/scaffolds/optionsPage/migrations/$className__snake$.sh index d62a3643..1ce81103 100644 --- a/src/main/scaffolds/optionsPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/optionsPage/migrations/$className__snake$.sh @@ -63,7 +63,12 @@ awk '/class/ {\ print;\ print "";\ print " def $className;format="decap"$: Option[AnswerRow] = userAnswers.get($className$Page) map {";\ - print " x => AnswerRow(\"$className;format="decap"$.checkYourAnswersLabel\", s\"$className;format="decap"$.\$x\", true, routes.$className$Controller.onPageLoad(CheckMode).url)";\ + print " x =>";\ + print " AnswerRow(";\ + print " HtmlFormat.escape(messages(\"$className;format="decap"$.checkYourAnswersLabel\")),";\ + print " HtmlFormat.escape(messages(s\"$className;format="decap"$.\$x\")),";\ + print " routes.$className$Controller.onPageLoad(CheckMode).url";\ + print " )" print " }";\ next }1' ../app/utils/CheckYourAnswersHelper.scala > tmp && mv tmp ../app/utils/CheckYourAnswersHelper.scala diff --git a/src/main/scaffolds/questionPage/migrations/$className__snake$.sh b/src/main/scaffolds/questionPage/migrations/$className__snake$.sh index f1248b6d..135cfb5a 100644 --- a/src/main/scaffolds/questionPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/questionPage/migrations/$className__snake$.sh @@ -69,7 +69,12 @@ awk '/class/ {\ print;\ print "";\ print " def $className;format="decap"$: Option[AnswerRow] = userAnswers.get($className$Page) map {";\ - print " x => AnswerRow(\"$className;format="decap"$.checkYourAnswersLabel\", s\"\${x.field1} \${x.field2}\", false, routes.$className$Controller.onPageLoad(CheckMode).url)";\ + print " x =>";\ + print " AnswerRow(";\ + print " HtmlFormat.escape(messages(\"$className;format="decap"$.checkYourAnswersLabel\")),";\ + print " HtmlFormat.escape(s\"\${x.field1} \${x.field2}\"),";\ + print " routes.$className$Controller.onPageLoad(CheckMode).url";\ + print " )" print " }";\ next }1' ../app/utils/CheckYourAnswersHelper.scala > tmp && mv tmp ../app/utils/CheckYourAnswersHelper.scala diff --git a/src/main/scaffolds/stringPage/migrations/$className__snake$.sh b/src/main/scaffolds/stringPage/migrations/$className__snake$.sh index c5e777ee..ccaef98a 100644 --- a/src/main/scaffolds/stringPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/stringPage/migrations/$className__snake$.sh @@ -52,7 +52,12 @@ awk '/class/ {\ print;\ print "";\ print " def $className;format="decap"$: Option[AnswerRow] = userAnswers.get($className$Page) map {";\ - print " x => AnswerRow(\"$className;format="decap"$.checkYourAnswersLabel\", s\"\$x\", false, routes.$className$Controller.onPageLoad(CheckMode).url)";\ + print " x =>";\ + print " AnswerRow(";\ + print " HtmlFormat.escape(messages(\"$className;format="decap"$.checkYourAnswersLabel\")),";\ + print " HtmlFormat.escape(x),";\ + print " routes.$className$Controller.onPageLoad(CheckMode).url";\ + print " )" print " }";\ next }1' ../app/utils/CheckYourAnswersHelper.scala > tmp && mv tmp ../app/utils/CheckYourAnswersHelper.scala diff --git a/src/main/scaffolds/yesNoPage/migrations/$className__snake$.sh b/src/main/scaffolds/yesNoPage/migrations/$className__snake$.sh index 338d0672..48af36da 100644 --- a/src/main/scaffolds/yesNoPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/yesNoPage/migrations/$className__snake$.sh @@ -51,7 +51,13 @@ awk '/class/ {\ print;\ print "";\ print " def $className;format="decap"$: Option[AnswerRow] = userAnswers.get($className$Page) map {";\ - print " x => AnswerRow(\"$className;format="decap"$.checkYourAnswersLabel\", if(x) \"site.yes\" else \"site.no\", true, routes.$className$Controller.onPageLoad(CheckMode).url)"; print " }";\ + print " x =>";\ + print " AnswerRow(";\ + print " HtmlFormat.escape(messages(\"$className;format="decap"$.checkYourAnswersLabel\")),";\ + print " yesOrNo(x),";\ + print " routes.$className$Controller.onPageLoad(CheckMode).url";\ + print " )" + print " }";\ next }1' ../app/utils/CheckYourAnswersHelper.scala > tmp && mv tmp ../app/utils/CheckYourAnswersHelper.scala echo "Migration $className;format="snake"$ completed" From 37d8e5ba18ddd50ed6226d1640a9064983cabc4d Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Thu, 25 Apr 2019 14:19:32 +0100 Subject: [PATCH 09/23] Allow field names to be specified on quesitonPage --- .../app/forms/$className$FormProvider.scala | 8 ++++---- .../questionPage/app/models/$className$.scala | 2 +- .../app/views/$className$View.scala.html | 8 ++++---- .../scaffolds/questionPage/default.properties | 2 ++ .../$className$ControllerSpec.scala | 8 ++++---- .../forms/$className$FormProviderSpec.scala | 16 +++++++-------- .../views/$className$ViewSpec.scala | 2 +- .../migrations/$className__snake$.sh | 20 +++++++++---------- 8 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/main/scaffolds/questionPage/app/forms/$className$FormProvider.scala b/src/main/scaffolds/questionPage/app/forms/$className$FormProvider.scala index 2095bb36..f9555b1c 100644 --- a/src/main/scaffolds/questionPage/app/forms/$className$FormProvider.scala +++ b/src/main/scaffolds/questionPage/app/forms/$className$FormProvider.scala @@ -11,10 +11,10 @@ class $className$FormProvider @Inject() extends Mappings { def apply(): Form[$className$] = Form( mapping( - "field1" -> text("$className;format="decap"$.error.field1.required") - .verifying(maxLength($field1MaxLength$, "$className;format="decap"$.error.field1.length")), - "field2" -> text("$className;format="decap"$.error.field2.required") - .verifying(maxLength($field2MaxLength$, "$className;format="decap"$.error.field2.length")) + "$field1Name$" -> text("$className;format="decap"$.error.$field1Name$.required") + .verifying(maxLength($field1MaxLength$, "$className;format="decap"$.error.$field1Name$.length")), + "$field2Name$" -> text("$className;format="decap"$.error.$field2Name$.required") + .verifying(maxLength($field2MaxLength$, "$className;format="decap"$.error.$field2Name$.length")) )($className$.apply)($className$.unapply) ) } diff --git a/src/main/scaffolds/questionPage/app/models/$className$.scala b/src/main/scaffolds/questionPage/app/models/$className$.scala index 80df74d0..5d439edb 100644 --- a/src/main/scaffolds/questionPage/app/models/$className$.scala +++ b/src/main/scaffolds/questionPage/app/models/$className$.scala @@ -2,7 +2,7 @@ package models import play.api.libs.json._ -case class $className$ (field1: String, field2: String) +case class $className$ ($field1Name$: String, $field2Name$: String) object $className$ { implicit val format = Json.format[$className$] diff --git a/src/main/scaffolds/questionPage/app/views/$className$View.scala.html b/src/main/scaffolds/questionPage/app/views/$className$View.scala.html index 5bbf57c7..0dd3e34e 100644 --- a/src/main/scaffolds/questionPage/app/views/$className$View.scala.html +++ b/src/main/scaffolds/questionPage/app/views/$className$View.scala.html @@ -21,13 +21,13 @@ @components.heading("$className;format="decap"$.heading") @components.input_text( - field = form("field1"), - label = messages("$className;format="decap"$.field1") + field = form("$field1Name$"), + label = messages("$className;format="decap"$.$field1Name$") ) @components.input_text( - field = form("field2"), - label = messages("$className;format="decap"$.field2") + field = form("$field2Name$"), + label = messages("$className;format="decap"$.$field2Name$") ) @components.submit_button() diff --git a/src/main/scaffolds/questionPage/default.properties b/src/main/scaffolds/questionPage/default.properties index fb5c63f2..da68c4f1 100644 --- a/src/main/scaffolds/questionPage/default.properties +++ b/src/main/scaffolds/questionPage/default.properties @@ -1,4 +1,6 @@ description = Generates a controller and view for a page with multiple questions className = MyNewPage +field1Name = field1 field1MaxLength = 100 +field2Name = field2 field2MaxLength = 100 diff --git a/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala index 74e7cfc5..b89500a1 100644 --- a/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala @@ -31,8 +31,8 @@ class $className$ControllerSpec extends SpecBase with MockitoSugar { userAnswersId, Json.obj( $className$Page.toString -> Json.obj( - "field1" -> "value 1", - "field2" -> "value 2" + "$field1Name$" -> "value 1", + "$field2Name$" -> "value 2" ) ) ) @@ -92,7 +92,7 @@ class $className$ControllerSpec extends SpecBase with MockitoSugar { val request = FakeRequest(POST, $className;format="decap"$Route) - .withFormUrlEncodedBody(("field1", "value 1"), ("field2", "value 2")) + .withFormUrlEncodedBody(("$field1Name$", "value 1"), ("$field2Name$", "value 2")) val result = route(application, request).value @@ -145,7 +145,7 @@ class $className$ControllerSpec extends SpecBase with MockitoSugar { val request = FakeRequest(POST, $className;format="decap"$Route) - .withFormUrlEncodedBody(("field1", "value 1"), ("field2", "value 2")) + .withFormUrlEncodedBody(("$field1Name$", "value 1"), ("$field2Name$", "value 2")) val result = route(application, request).value diff --git a/src/main/scaffolds/questionPage/generated-test/forms/$className$FormProviderSpec.scala b/src/main/scaffolds/questionPage/generated-test/forms/$className$FormProviderSpec.scala index 335ff2c6..e351be9a 100644 --- a/src/main/scaffolds/questionPage/generated-test/forms/$className$FormProviderSpec.scala +++ b/src/main/scaffolds/questionPage/generated-test/forms/$className$FormProviderSpec.scala @@ -7,11 +7,11 @@ class $className$FormProviderSpec extends StringFieldBehaviours { val form = new $className$FormProvider()() - ".field1" must { + ".$field1Name$" must { - val fieldName = "field1" - val requiredKey = "$className;format="decap"$.error.field1.required" - val lengthKey = "$className;format="decap"$.error.field1.length" + val fieldName = "$field1Name$" + val requiredKey = "$className;format="decap"$.error.$field1Name$.required" + val lengthKey = "$className;format="decap"$.error.$field1Name$.length" val maxLength = $field1MaxLength$ behave like fieldThatBindsValidData( @@ -34,11 +34,11 @@ class $className$FormProviderSpec extends StringFieldBehaviours { ) } - ".field2" must { + ".$field2Name$" must { - val fieldName = "field2" - val requiredKey = "$className;format="decap"$.error.field2.required" - val lengthKey = "$className;format="decap"$.error.field2.length" + val fieldName = "$field2Name$" + val requiredKey = "$className;format="decap"$.error.$field2Name$.required" + val lengthKey = "$className;format="decap"$.error.$field2Name$.length" val maxLength = $field2MaxLength$ behave like fieldThatBindsValidData( diff --git a/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala index 6d669a8e..5b819ed0 100644 --- a/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala +++ b/src/main/scaffolds/questionPage/generated-test/views/$className$ViewSpec.scala @@ -31,7 +31,7 @@ class $className$ViewSpec extends QuestionViewBehaviours[$className$] { applyView, messageKeyPrefix, routes.$className$Controller.onSubmit(NormalMode).url, - "field1", "field2" + "$field1Name$", "$field2Name$" ) } } diff --git a/src/main/scaffolds/questionPage/migrations/$className__snake$.sh b/src/main/scaffolds/questionPage/migrations/$className__snake$.sh index 135cfb5a..c3bc9a30 100644 --- a/src/main/scaffolds/questionPage/migrations/$className__snake$.sh +++ b/src/main/scaffolds/questionPage/migrations/$className__snake$.sh @@ -16,13 +16,13 @@ echo "Adding messages to conf.messages" echo "" >> ../conf/messages.en echo "$className;format="decap"$.title = $className;format="decap"$" >> ../conf/messages.en echo "$className;format="decap"$.heading = $className;format="decap"$" >> ../conf/messages.en -echo "$className;format="decap"$.field1 = Field 1" >> ../conf/messages.en -echo "$className;format="decap"$.field2 = Field 2" >> ../conf/messages.en +echo "$className;format="decap"$.$field1Name$ = $field1Name$" >> ../conf/messages.en +echo "$className;format="decap"$.$field2Name$ = $field2Name$" >> ../conf/messages.en echo "$className;format="decap"$.checkYourAnswersLabel = $className;format="decap"$" >> ../conf/messages.en -echo "$className;format="decap"$.error.field1.required = Enter field1" >> ../conf/messages.en -echo "$className;format="decap"$.error.field2.required = Enter field2" >> ../conf/messages.en -echo "$className;format="decap"$.error.field1.length = field1 must be $field1MaxLength$ characters or less" >> ../conf/messages.en -echo "$className;format="decap"$.error.field2.length = field2 must be $field2MaxLength$ characters or less" >> ../conf/messages.en +echo "$className;format="decap"$.error.$field1Name$.required = Enter $field1Name$" >> ../conf/messages.en +echo "$className;format="decap"$.error.$field2Name$.required = Enter $field2Name$" >> ../conf/messages.en +echo "$className;format="decap"$.error.$field1Name$.length = $field1Name$ must be $field1MaxLength$ characters or less" >> ../conf/messages.en +echo "$className;format="decap"$.error.$field2Name$.length = $field2Name$ must be $field2MaxLength$ characters or less" >> ../conf/messages.en echo "Adding to UserAnswersEntryGenerators" awk '/trait UserAnswersEntryGenerators/ {\ @@ -52,9 +52,9 @@ awk '/trait ModelGenerators/ {\ print " implicit lazy val arbitrary$className$: Arbitrary[$className$] =";\ print " Arbitrary {";\ print " for {";\ - print " field1 <- arbitrary[String]";\ - print " field2 <- arbitrary[String]";\ - print " } yield $className$(field1, field2)";\ + print " $field1Name$ <- arbitrary[String]";\ + print " $field2Name$ <- arbitrary[String]";\ + print " } yield $className$($field1Name$, $field2Name$)";\ print " }";\ next }1' ../test/generators/ModelGenerators.scala > tmp && mv tmp ../test/generators/ModelGenerators.scala @@ -72,7 +72,7 @@ awk '/class/ {\ print " x =>";\ print " AnswerRow(";\ print " HtmlFormat.escape(messages(\"$className;format="decap"$.checkYourAnswersLabel\")),";\ - print " HtmlFormat.escape(s\"\${x.field1} \${x.field2}\"),";\ + print " HtmlFormat.escape(s\"\${x.$field1Name$} \${x.$field2Name$}\"),";\ print " routes.$className$Controller.onPageLoad(CheckMode).url";\ print " )" print " }";\ From ae1ef57ad1e49c5b46b9e9824096f1c5cb295082 Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Wed, 1 May 2019 08:47:09 +0100 Subject: [PATCH 10/23] Update dependencies --- src/main/g8/build.sbt | 1 + src/main/g8/project/AppDependencies.scala | 19 +++++++++++-------- .../actions/DataRetrievalActionSpec.scala | 2 +- .../g8/test/filters/WhitelistFilterSpec.scala | 6 +++--- .../forms/behaviours/FieldBehaviours.scala | 4 ++-- src/main/g8/test/models/RichJsValueSpec.scala | 4 ++-- .../pages/behaviours/PageBehaviours.scala | 4 ++-- .../$className$ControllerSpec.scala | 2 +- .../$className$ControllerSpec.scala | 2 +- .../models/$className$Spec.scala | 4 ++-- .../$className$ControllerSpec.scala | 2 +- .../$className$ControllerSpec.scala | 2 +- .../$className$ControllerSpec.scala | 2 +- 13 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/main/g8/build.sbt b/src/main/g8/build.sbt index 6f04308a..c46439af 100644 --- a/src/main/g8/build.sbt +++ b/src/main/g8/build.sbt @@ -34,6 +34,7 @@ lazy val root = (project in file(".")) ScoverageKeys.coverageHighlighting := true, scalacOptions ++= Seq("-feature"), libraryDependencies ++= AppDependencies(), + dependencyOverrides ++= AppDependencies.overrides, retrieveManaged := true, evictionWarningOptions in update := EvictionWarningOptions.default.withWarnScalaVersionEviction(false), diff --git a/src/main/g8/project/AppDependencies.scala b/src/main/g8/project/AppDependencies.scala index adcd0900..6971f7ec 100644 --- a/src/main/g8/project/AppDependencies.scala +++ b/src/main/g8/project/AppDependencies.scala @@ -7,24 +7,27 @@ object AppDependencies { play.sbt.PlayImport.ws, "org.reactivemongo" %% "play2-reactivemongo" % "0.16.0-play26", "uk.gov.hmrc" %% "logback-json-logger" % "3.1.0", - "uk.gov.hmrc" %% "govuk-template" % "5.25.0-play-26", - "uk.gov.hmrc" %% "play-health" % "3.7.0-play-26", - "uk.gov.hmrc" %% "play-ui" % "7.25.0-play-26", + "uk.gov.hmrc" %% "govuk-template" % "5.35.0-play-26", + "uk.gov.hmrc" %% "play-health" % "3.14.0-play-26", + "uk.gov.hmrc" %% "play-ui" % "7.39.0-play-26", "uk.gov.hmrc" %% "play-conditional-form-mapping" % "0.2.0", - "uk.gov.hmrc" %% "bootstrap-play-26" % "0.27.0", + "uk.gov.hmrc" %% "bootstrap-play-26" % "0.39.0", "uk.gov.hmrc" %% "play-whitelist-filter" % "2.0.0" ) val test = Seq( - "uk.gov.hmrc" %% "hmrctest" % "3.1.0", - "org.scalatest" %% "scalatest" % "3.0.4", - "org.scalatestplus.play" %% "scalatestplus-play" % "2.0.1", + "org.scalatest" %% "scalatest" % "3.0.7", + "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.2", "org.pegdown" % "pegdown" % "1.6.0", "org.jsoup" % "jsoup" % "1.10.3", "com.typesafe.play" %% "play-test" % PlayVersion.current, "org.mockito" % "mockito-all" % "1.10.19", - "org.scalacheck" %% "scalacheck" % "1.13.4" + "org.scalacheck" %% "scalacheck" % "1.14.0" ).map(_ % Test) + val overrides: Set[ModuleID] = Set( + "org.reactivemongo" %% "reactivemongo" % "0.16.1" + ) + def apply(): Seq[ModuleID] = compile ++ test } diff --git a/src/main/g8/test/controllers/actions/DataRetrievalActionSpec.scala b/src/main/g8/test/controllers/actions/DataRetrievalActionSpec.scala index acbfe8ae..f0b79adc 100644 --- a/src/main/g8/test/controllers/actions/DataRetrievalActionSpec.scala +++ b/src/main/g8/test/controllers/actions/DataRetrievalActionSpec.scala @@ -5,7 +5,7 @@ import models.UserAnswers import models.requests.{IdentifierRequest, OptionalDataRequest} import org.mockito.Mockito._ import org.scalatest.concurrent.ScalaFutures -import org.scalatest.mockito.MockitoSugar +import org.scalatestplus.mockito.MockitoSugar import play.api.libs.json.Json import repositories.SessionRepository diff --git a/src/main/g8/test/filters/WhitelistFilterSpec.scala b/src/main/g8/test/filters/WhitelistFilterSpec.scala index e5ba63d4..0f1e4f81 100644 --- a/src/main/g8/test/filters/WhitelistFilterSpec.scala +++ b/src/main/g8/test/filters/WhitelistFilterSpec.scala @@ -5,13 +5,13 @@ import com.typesafe.config.ConfigException import generators.Generators import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen -import org.scalatest.mockito.MockitoSugar -import org.scalatest.prop.PropertyChecks +import org.scalatestplus.mockito.MockitoSugar +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import org.scalatest.{FreeSpec, MustMatchers} import play.api.Configuration import play.api.mvc.Call -class WhitelistFilterSpec extends FreeSpec with MustMatchers with PropertyChecks with MockitoSugar with Generators { +class WhitelistFilterSpec extends FreeSpec with MustMatchers with ScalaCheckPropertyChecks with MockitoSugar with Generators { val mockMaterializer = mock[Materializer] diff --git a/src/main/g8/test/forms/behaviours/FieldBehaviours.scala b/src/main/g8/test/forms/behaviours/FieldBehaviours.scala index d8746906..4cc47663 100644 --- a/src/main/g8/test/forms/behaviours/FieldBehaviours.scala +++ b/src/main/g8/test/forms/behaviours/FieldBehaviours.scala @@ -3,10 +3,10 @@ package forms.behaviours import forms.FormSpec import generators.Generators import org.scalacheck.Gen -import org.scalatest.prop.PropertyChecks +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import play.api.data.{Form, FormError} -trait FieldBehaviours extends FormSpec with PropertyChecks with Generators { +trait FieldBehaviours extends FormSpec with ScalaCheckPropertyChecks with Generators { def fieldThatBindsValidData(form: Form[_], fieldName: String, diff --git a/src/main/g8/test/models/RichJsValueSpec.scala b/src/main/g8/test/models/RichJsValueSpec.scala index aa4e922c..291b204b 100644 --- a/src/main/g8/test/models/RichJsValueSpec.scala +++ b/src/main/g8/test/models/RichJsValueSpec.scala @@ -2,11 +2,11 @@ package models import generators.ModelGenerators import org.scalacheck.{Gen, Shrink} -import org.scalatest.prop.PropertyChecks +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import org.scalatest.{FreeSpec, MustMatchers, OptionValues} import play.api.libs.json._ -class RichJsValueSpec extends FreeSpec with MustMatchers with PropertyChecks with OptionValues with ModelGenerators { +class RichJsValueSpec extends FreeSpec with MustMatchers with ScalaCheckPropertyChecks with OptionValues with ModelGenerators { implicit def dontShrink[A]: Shrink[A] = Shrink.shrinkAny diff --git a/src/main/g8/test/pages/behaviours/PageBehaviours.scala b/src/main/g8/test/pages/behaviours/PageBehaviours.scala index b82b57c7..15359b38 100644 --- a/src/main/g8/test/pages/behaviours/PageBehaviours.scala +++ b/src/main/g8/test/pages/behaviours/PageBehaviours.scala @@ -4,12 +4,12 @@ import generators.Generators import models.UserAnswers import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.{Arbitrary, Gen} -import org.scalatest.prop.PropertyChecks +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import org.scalatest.{MustMatchers, OptionValues, TryValues, WordSpec} import pages.QuestionPage import play.api.libs.json._ -trait PageBehaviours extends WordSpec with MustMatchers with PropertyChecks with Generators with OptionValues with TryValues { +trait PageBehaviours extends WordSpec with MustMatchers with ScalaCheckPropertyChecks with Generators with OptionValues with TryValues { class BeRetrievable[A] { def apply[P <: QuestionPage[A]](genP: Gen[P])(implicit ev1: Arbitrary[A], ev2: Format[A]): Unit = { diff --git a/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala index b1a361bc..5a7515de 100644 --- a/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/intPage/generated-test/controllers/$className$ControllerSpec.scala @@ -6,7 +6,7 @@ import models.{NormalMode, UserAnswers} import navigation.{FakeNavigator, Navigator} import org.mockito.Matchers.any import org.mockito.Mockito.when -import org.scalatest.mockito.MockitoSugar +import org.scalatestplus.mockito.MockitoSugar import pages.$className$Page import play.api.inject.bind import play.api.libs.json.{JsNumber, Json} diff --git a/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala index 4df41864..655d3083 100644 --- a/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/optionsPage/generated-test/controllers/$className$ControllerSpec.scala @@ -6,7 +6,7 @@ import models.{NormalMode, $className$, UserAnswers} import navigation.{FakeNavigator, Navigator} import org.mockito.Matchers.any import org.mockito.Mockito.when -import org.scalatest.mockito.MockitoSugar +import org.scalatestplus.mockito.MockitoSugar import pages.$className$Page import play.api.inject.bind import play.api.libs.json.{JsString, Json} diff --git a/src/main/scaffolds/optionsPage/generated-test/models/$className$Spec.scala b/src/main/scaffolds/optionsPage/generated-test/models/$className$Spec.scala index 0a66378b..bc76f858 100644 --- a/src/main/scaffolds/optionsPage/generated-test/models/$className$Spec.scala +++ b/src/main/scaffolds/optionsPage/generated-test/models/$className$Spec.scala @@ -2,11 +2,11 @@ package models import org.scalacheck.Arbitrary.arbitrary import org.scalacheck.Gen -import org.scalatest.prop.PropertyChecks +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import org.scalatest.{MustMatchers, OptionValues, WordSpec} import play.api.libs.json.{JsError, JsString, Json} -class $className$Spec extends WordSpec with MustMatchers with PropertyChecks with OptionValues { +class $className$Spec extends WordSpec with MustMatchers with ScalaCheckPropertyChecks with OptionValues { "$className$" must { diff --git a/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala index b89500a1..a048c4c3 100644 --- a/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/questionPage/generated-test/controllers/$className$ControllerSpec.scala @@ -6,7 +6,7 @@ import models.{NormalMode, $className$, UserAnswers} import navigation.{FakeNavigator, Navigator} import org.mockito.Matchers.any import org.mockito.Mockito.when -import org.scalatest.mockito.MockitoSugar +import org.scalatestplus.mockito.MockitoSugar import pages.$className$Page import play.api.inject.bind import play.api.libs.json.Json diff --git a/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala index 6904fbb1..d31eb9b0 100644 --- a/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/stringPage/generated-test/controllers/$className$ControllerSpec.scala @@ -6,7 +6,7 @@ import models.{NormalMode, UserAnswers} import navigation.{FakeNavigator, Navigator} import org.mockito.Matchers.any import org.mockito.Mockito.when -import org.scalatest.mockito.MockitoSugar +import org.scalatestplus.mockito.MockitoSugar import pages.$className$Page import play.api.inject.bind import play.api.libs.json.{JsString, Json} diff --git a/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala index a77feea9..36a5a2d7 100644 --- a/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/yesNoPage/generated-test/controllers/$className$ControllerSpec.scala @@ -6,7 +6,7 @@ import models.{NormalMode, UserAnswers} import navigation.{FakeNavigator, Navigator} import org.mockito.Matchers.any import org.mockito.Mockito.when -import org.scalatest.mockito.MockitoSugar +import org.scalatestplus.mockito.MockitoSugar import pages.$className$Page import play.api.inject.bind import play.api.libs.json.{JsBoolean, Json} From 6e2b1fac47e7b2cf58ce202906234b8dd22a65c5 Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Wed, 1 May 2019 09:03:15 +0100 Subject: [PATCH 11/23] Update plugins --- src/main/g8/project/plugins.sbt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/g8/project/plugins.sbt b/src/main/g8/project/plugins.sbt index 89223c4b..b9aad10a 100644 --- a/src/main/g8/project/plugins.sbt +++ b/src/main/g8/project/plugins.sbt @@ -4,19 +4,19 @@ resolvers += "HMRC Releases" at "https://dl.bintray.com/hmrc/releases" resolvers += "Typesafe Releases" at "http://repo.typesafe.com/typesafe/releases/" -addSbtPlugin("uk.gov.hmrc" % "sbt-auto-build" % "1.13.0") +addSbtPlugin("uk.gov.hmrc" % "sbt-auto-build" % "1.16.0") -addSbtPlugin("uk.gov.hmrc" % "sbt-git-versioning" % "1.15.0") +addSbtPlugin("uk.gov.hmrc" % "sbt-git-versioning" % "1.19.0") -addSbtPlugin("uk.gov.hmrc" % "sbt-distributables" % "1.1.0") +addSbtPlugin("uk.gov.hmrc" % "sbt-distributables" % "1.5.0") -addSbtPlugin("uk.gov.hmrc" % "sbt-artifactory" % "0.13.0") +addSbtPlugin("uk.gov.hmrc" % "sbt-artifactory" % "0.19.0") -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.15") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.20") -addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.9.0") +addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") addSbtPlugin("org.irundaia.sbt" % "sbt-sassify" % "1.4.11") From 535f42748ccdfc608b6075e53316b1d73848f4f0 Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Wed, 1 May 2019 09:51:59 +0100 Subject: [PATCH 12/23] Fix auth action recover block --- .../g8/app/controllers/actions/IdentifierAction.scala | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/g8/app/controllers/actions/IdentifierAction.scala b/src/main/g8/app/controllers/actions/IdentifierAction.scala index 6d51ac13..ea215284 100644 --- a/src/main/g8/app/controllers/actions/IdentifierAction.scala +++ b/src/main/g8/app/controllers/actions/IdentifierAction.scala @@ -7,12 +7,14 @@ import models.requests.IdentifierRequest import play.api.mvc.Results._ import play.api.mvc._ import uk.gov.hmrc.auth.core._ -import uk.gov.hmrc.auth.core.retrieve.Retrievals +import uk.gov.hmrc.auth.core.retrieve.v2.Retrievals import uk.gov.hmrc.http.{HeaderCarrier, UnauthorizedException} import uk.gov.hmrc.play.HeaderCarrierConverter import scala.concurrent.{ExecutionContext, Future} +trait IdentifierAction extends ActionBuilder[IdentifierRequest, AnyContent] with ActionFunction[Request, IdentifierRequest] + class AuthenticatedIdentifierAction @Inject()( override val authConnector: AuthConnector, config: FrontendAppConfig, @@ -31,15 +33,12 @@ class AuthenticatedIdentifierAction @Inject()( } recover { case _: NoActiveSession => Redirect(config.loginUrl, Map("continue" -> Seq(config.loginContinueUrl))) - case _ => + case _: AuthorisationException => Redirect(routes.UnauthorisedController.onPageLoad()) - } } } -trait IdentifierAction extends ActionBuilder[IdentifierRequest, AnyContent] with ActionFunction[Request, IdentifierRequest] - class SessionIdentifierAction @Inject()( config: FrontendAppConfig, val parser: BodyParsers.Default From b1b5d7076412eeba94c5728d3db2b0c57012bde2 Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Wed, 1 May 2019 10:52:51 +0100 Subject: [PATCH 13/23] Refactor to avoid compiler warning in SessionIdFilterSpec --- .../g8/test/filters/SessionIdFilterSpec.scala | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/main/g8/test/filters/SessionIdFilterSpec.scala b/src/main/g8/test/filters/SessionIdFilterSpec.scala index efed1bd4..ba886fd0 100644 --- a/src/main/g8/test/filters/SessionIdFilterSpec.scala +++ b/src/main/g8/test/filters/SessionIdFilterSpec.scala @@ -5,8 +5,8 @@ import java.util.UUID import akka.stream.Materializer import com.google.inject.Inject import org.scalatest.{MustMatchers, OptionValues, WordSpec} -import org.scalatestplus.play.OneAppPerSuite -import play.api.Application +import org.scalatestplus.play.components.OneAppPerSuiteWithComponents +import play.api.{Application, BuiltInComponents, BuiltInComponentsFromContext, NoHttpFiltersComponents} import play.api.inject.guice.GuiceApplicationBuilder import play.api.libs.json.Json import play.api.mvc.{Action, Results, SessionCookieBaker} @@ -29,16 +29,16 @@ object SessionIdFilterSpec { } -class SessionIdFilterSpec extends WordSpec with MustMatchers with OneAppPerSuite with OptionValues { +class SessionIdFilterSpec extends WordSpec with MustMatchers with OptionValues with OneAppPerSuiteWithComponents { - import SessionIdFilterSpec._ - - val router: Router = { + override def components: BuiltInComponents = new BuiltInComponentsFromContext(context) with NoHttpFiltersComponents { + import play.api.mvc.Results + import play.api.routing.Router import play.api.routing.sird._ - Router.from { - case GET(p"/test") => Action { + lazy val router: Router = Router.from { + case GET(p"/test") => defaultActionBuilder.apply { request => val fromHeader = request.headers.get(HeaderNames.xSessionId).getOrElse("") val fromSession = request.session.get(SessionKeys.sessionId).getOrElse("") @@ -49,13 +49,15 @@ class SessionIdFilterSpec extends WordSpec with MustMatchers with OneAppPerSuite ) ) } - case GET(p"/test2") => Action { + case GET(p"/test2") => defaultActionBuilder.apply { implicit request => Results.Ok.addingToSession("foo" -> "bar") } } } + import SessionIdFilterSpec._ + override lazy val app: Application = { import play.api.inject._ @@ -67,7 +69,7 @@ class SessionIdFilterSpec extends WordSpec with MustMatchers with OneAppPerSuite .configure( "play.filters.disabled" -> List("uk.gov.hmrc.play.bootstrap.filters.frontend.crypto.SessionCookieCryptoFilter") ) - .router(router) + .router(components.router) .build() } @@ -108,4 +110,4 @@ class SessionIdFilterSpec extends WordSpec with MustMatchers with OneAppPerSuite session(result).data must contain("foo" -> "bar") } } -} +} \ No newline at end of file From 040ef8c5f5595dcf56989baa90d693479f55a4eb Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Wed, 1 May 2019 14:20:54 +0100 Subject: [PATCH 14/23] Fix textarea whitespace issue --- src/main/g8/app/views/components/input_textarea.scala.html | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/main/g8/app/views/components/input_textarea.scala.html b/src/main/g8/app/views/components/input_textarea.scala.html index 15a4c5bf..6fb9b4fc 100644 --- a/src/main/g8/app/views/components/input_textarea.scala.html +++ b/src/main/g8/app/views/components/input_textarea.scala.html @@ -23,10 +23,7 @@ id="@{field.id}" name="@{field.id}" @if(field.hasErrors) { aria-describedby="error-message-@{field.id}-input" } - rows="5"> - - @{field.value} - + rows="5" + >@{field.value} - From 55d33bfc52ddd78b20bd22de70f700ec76704f47 Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Wed, 1 May 2019 14:43:39 +0100 Subject: [PATCH 15/23] Remove unsafe-inline and data: from CSP --- src/main/g8/conf/application.conf | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/g8/conf/application.conf b/src/main/g8/conf/application.conf index 92460512..4efe9cad 100644 --- a/src/main/g8/conf/application.conf +++ b/src/main/g8/conf/application.conf @@ -4,7 +4,7 @@ appName="$name$" play.http.router=prod.Routes -play.filters.headers.contentSecurityPolicy= "default-src 'self' 'unsafe-inline' localhost:9000 localhost:9032 localhost:9250 www.google-analytics.com data:" +play.filters.headers.contentSecurityPolicy = "default-src 'self' localhost:9000 localhost:9032 localhost:9250 www.google-analytics.com" play.http.requestHandler = "uk.gov.hmrc.play.bootstrap.http.RequestHandler" play.http.errorHandler = "handlers.ErrorHandler" @@ -66,17 +66,17 @@ microservice { } metrics { - name = \${appName} - rateUnit = SECONDS + name = \${appName} + rateUnit = SECONDS durationUnit = SECONDS - showSamples = true - jvm = true - enabled = false + showSamples = true + jvm = true + enabled = false } auditing { - enabled=false - traceRequests=true + enabled = false + traceRequests = true consumer { baseUri { host = localhost @@ -87,7 +87,7 @@ auditing { google-analytics { token = N/A - host = auto + host = auto } assets { @@ -101,11 +101,11 @@ contact-frontend { } mongodb { - uri = "mongodb://localhost:27017/"\${appName} + uri = "mongodb://localhost:27017/"\${appName} timeToLiveInSeconds = 900 } urls { - login = "http://localhost:9949/auth-login-stub/gg-sign-in" + login = "http://localhost:9949/auth-login-stub/gg-sign-in" loginContinue = "http://localhost:$port$/$name$" } From d906e7076b5e1b3633ff6a528c35b54ef9cde8ab Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Wed, 1 May 2019 14:43:53 +0100 Subject: [PATCH 16/23] Update assets frontend version --- src/main/g8/conf/application.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/g8/conf/application.conf b/src/main/g8/conf/application.conf index 4efe9cad..9b42b058 100644 --- a/src/main/g8/conf/application.conf +++ b/src/main/g8/conf/application.conf @@ -91,7 +91,7 @@ google-analytics { } assets { - version = "3.5.0" + version = "3.11.0" version = \${?ASSETS_FRONTEND_VERSION} url = "http://localhost:9032/assets/" } From dd26c068bf4bd28ea5460ca24494facf929e29db Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Mon, 20 May 2019 11:27:10 +0100 Subject: [PATCH 17/23] Update navigator --- src/main/g8/app/navigation/Navigator.scala | 18 +++++++++--------- .../behaviours/OptionFieldBehaviours.scala | 2 +- src/main/g8/test/generators/Generators.scala | 2 +- .../g8/test/navigation/FakeNavigator.scala | 4 +++- .../g8/test/navigation/NavigatorSpec.scala | 4 ++-- .../controllers/$className$Controller.scala | 8 +++----- .../controllers/$className$Controller.scala | 12 +++++------- .../optionsPage/app/models/$className$.scala | 6 +++--- .../controllers/$className$Controller.scala | 8 +++----- .../controllers/$className$Controller.scala | 8 +++----- .../controllers/$className$Controller.scala | 14 ++++++-------- 11 files changed, 39 insertions(+), 47 deletions(-) diff --git a/src/main/g8/app/navigation/Navigator.scala b/src/main/g8/app/navigation/Navigator.scala index 6fecf560..612f34d8 100644 --- a/src/main/g8/app/navigation/Navigator.scala +++ b/src/main/g8/app/navigation/Navigator.scala @@ -10,18 +10,18 @@ import models._ @Singleton class Navigator @Inject()() { - private val routeMap: Map[Page, UserAnswers => Call] = Map( - - ) - - private val checkRouteMap: Map[Page, UserAnswers => Call] = Map( + private val normalRoutes: Page => UserAnswers => Call = { + case _ => _ => routes.IndexController.onPageLoad() + } - ) + private val checkRouteMap: Page => UserAnswers => Call = { + case _ => _ => routes.CheckYourAnswersController.onPageLoad() + } - def nextPage(page: Page, mode: Mode): UserAnswers => Call = mode match { + def nextPage(page: Page, mode: Mode, userAnswers: UserAnswers): Call = mode match { case NormalMode => - routeMap.getOrElse(page, _ => routes.IndexController.onPageLoad()) + normalRoutes(page)(userAnswers) case CheckMode => - checkRouteMap.getOrElse(page, _ => routes.CheckYourAnswersController.onPageLoad()) + checkRouteMap(page)(userAnswers) } } diff --git a/src/main/g8/test/forms/behaviours/OptionFieldBehaviours.scala b/src/main/g8/test/forms/behaviours/OptionFieldBehaviours.scala index 956fc39d..1236b468 100644 --- a/src/main/g8/test/forms/behaviours/OptionFieldBehaviours.scala +++ b/src/main/g8/test/forms/behaviours/OptionFieldBehaviours.scala @@ -6,7 +6,7 @@ class OptionFieldBehaviours extends FieldBehaviours { def optionsField[T](form: Form[_], fieldName: String, - validValues: Set[T], + validValues: Seq[T], invalidError: FormError): Unit = { diff --git a/src/main/g8/test/generators/Generators.scala b/src/main/g8/test/generators/Generators.scala index 733a6e98..eb40b5c9 100644 --- a/src/main/g8/test/generators/Generators.scala +++ b/src/main/g8/test/generators/Generators.scala @@ -79,7 +79,7 @@ trait Generators extends UserAnswersGenerator with PageGenerators with ModelGene chars <- listOfN(length, arbitrary[Char]) } yield chars.mkString - def stringsExceptSpecificValues(excluded: Set[String]): Gen[String] = + def stringsExceptSpecificValues(excluded: Seq[String]): Gen[String] = nonEmptyString suchThat (!excluded.contains(_)) def oneOf[T](xs: Seq[Gen[T]]): Gen[T] = diff --git a/src/main/g8/test/navigation/FakeNavigator.scala b/src/main/g8/test/navigation/FakeNavigator.scala index ec798f2d..9094d760 100644 --- a/src/main/g8/test/navigation/FakeNavigator.scala +++ b/src/main/g8/test/navigation/FakeNavigator.scala @@ -5,5 +5,7 @@ import pages._ import models.{Mode, NormalMode, UserAnswers} class FakeNavigator(desiredRoute: Call, mode: Mode = NormalMode) extends Navigator { - override def nextPage(page: Page, mode: Mode): UserAnswers => Call = _ => desiredRoute + + override def nextPage(page: Page, mode: Mode, userAnswers: UserAnswers): Call = + desiredRoute } diff --git a/src/main/g8/test/navigation/NavigatorSpec.scala b/src/main/g8/test/navigation/NavigatorSpec.scala index e3edd51c..2580b096 100644 --- a/src/main/g8/test/navigation/NavigatorSpec.scala +++ b/src/main/g8/test/navigation/NavigatorSpec.scala @@ -16,7 +16,7 @@ class NavigatorSpec extends SpecBase { "go to Index from a page that doesn't exist in the route map" in { case object UnknownPage extends Page - navigator.nextPage(UnknownPage, NormalMode)(UserAnswers("id")) mustBe routes.IndexController.onPageLoad() + navigator.nextPage(UnknownPage, NormalMode, UserAnswers("id")) mustBe routes.IndexController.onPageLoad() } } @@ -25,7 +25,7 @@ class NavigatorSpec extends SpecBase { "go to CheckYourAnswers from a page that doesn't exist in the edit route map" in { case object UnknownPage extends Page - navigator.nextPage(UnknownPage, CheckMode)(UserAnswers("id")) mustBe routes.CheckYourAnswersController.onPageLoad() + navigator.nextPage(UnknownPage, CheckMode, UserAnswers("id")) mustBe routes.CheckYourAnswersController.onPageLoad() } } } diff --git a/src/main/scaffolds/intPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/intPage/app/controllers/$className$Controller.scala index 5c7d8d6e..8aa6e555 100644 --- a/src/main/scaffolds/intPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/intPage/app/controllers/$className$Controller.scala @@ -6,7 +6,6 @@ import javax.inject.Inject import models.Mode import navigation.Navigator import pages.$className$Page -import play.api.data.Form import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import repositories.SessionRepository @@ -44,15 +43,14 @@ class $className$Controller @Inject()( implicit request => form.bindFromRequest().fold( - (formWithErrors: Form[_]) => + formWithErrors => Future.successful(BadRequest(view(formWithErrors, mode))), - value => { + value => for { updatedAnswers <- Future.fromTry(request.userAnswers.set($className$Page, value)) _ <- sessionRepository.set(updatedAnswers) - } yield Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) - } + } yield Redirect(navigator.nextPage($className$Page, mode, updatedAnswers)) ) } } diff --git a/src/main/scaffolds/optionsPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/optionsPage/app/controllers/$className$Controller.scala index 29b608ad..eba286fd 100644 --- a/src/main/scaffolds/optionsPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/optionsPage/app/controllers/$className$Controller.scala @@ -3,10 +3,9 @@ package controllers import controllers.actions._ import forms.$className$FormProvider import javax.inject.Inject -import models.{Enumerable, Mode} +import models.Mode import navigation.Navigator import pages.$className$Page -import play.api.data.Form import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import repositories.SessionRepository @@ -25,7 +24,7 @@ class $className$Controller @Inject()( formProvider: $className$FormProvider, val controllerComponents: MessagesControllerComponents, view: $className$View - )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport with Enumerable.Implicits { + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { val form = formProvider() @@ -44,15 +43,14 @@ class $className$Controller @Inject()( implicit request => form.bindFromRequest().fold( - (formWithErrors: Form[_]) => + formWithErrors => Future.successful(BadRequest(view(formWithErrors, mode))), - value => { + value => for { updatedAnswers <- Future.fromTry(request.userAnswers.set($className$Page, value)) _ <- sessionRepository.set(updatedAnswers) - } yield Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) - } + } yield Redirect(navigator.nextPage($className$Page, mode, updatedAnswers)) ) } } diff --git a/src/main/scaffolds/optionsPage/app/models/$className$.scala b/src/main/scaffolds/optionsPage/app/models/$className$.scala index 15d45e37..efa99874 100644 --- a/src/main/scaffolds/optionsPage/app/models/$className$.scala +++ b/src/main/scaffolds/optionsPage/app/models/$className$.scala @@ -10,15 +10,15 @@ object $className$ extends Enumerable.Implicits { case object $option1key;format="Camel"$ extends WithName("$option1key;format="decap"$") with $className$ case object $option2key;format="Camel"$ extends WithName("$option2key;format="decap"$") with $className$ - val values: Set[$className$] = Set( + val values: Seq[$className$] = Seq( $option1key;format="Camel"$, $option2key;format="Camel"$ ) - val options: Set[RadioOption] = values.map { + val options: Seq[RadioOption] = values.map { value => RadioOption("$className;format="decap"$", value.toString) } implicit val enumerable: Enumerable[$className$] = - Enumerable(values.toSeq.map(v => v.toString -> v): _*) + Enumerable(values.map(v => v.toString -> v): _*) } diff --git a/src/main/scaffolds/questionPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/questionPage/app/controllers/$className$Controller.scala index 1bf552a0..85156bf7 100644 --- a/src/main/scaffolds/questionPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/questionPage/app/controllers/$className$Controller.scala @@ -6,7 +6,6 @@ import javax.inject.Inject import models.Mode import navigation.Navigator import pages.$className$Page -import play.api.data.Form import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import repositories.SessionRepository @@ -44,15 +43,14 @@ class $className$Controller @Inject()( implicit request => form.bindFromRequest().fold( - (formWithErrors: Form[_]) => + formWithErrors => Future.successful(BadRequest(view(formWithErrors, mode))), - value => { + value => for { updatedAnswers <- Future.fromTry(request.userAnswers.set($className$Page, value)) _ <- sessionRepository.set(updatedAnswers) - } yield Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) - } + } yield Redirect(navigator.nextPage($className$Page, mode, updatedAnswers)) ) } } diff --git a/src/main/scaffolds/stringPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/stringPage/app/controllers/$className$Controller.scala index d3b14ba5..ddabcd39 100644 --- a/src/main/scaffolds/stringPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/stringPage/app/controllers/$className$Controller.scala @@ -6,7 +6,6 @@ import javax.inject.Inject import models.Mode import navigation.Navigator import pages.$className$Page -import play.api.data.Form import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import repositories.SessionRepository @@ -44,15 +43,14 @@ class $className$Controller @Inject()( implicit request => form.bindFromRequest().fold( - (formWithErrors: Form[_]) => + formWithErrors => Future.successful(BadRequest(view(formWithErrors, mode))), - value => { + value => for { updatedAnswers <- Future.fromTry(request.userAnswers.set($className$Page, value)) _ <- sessionRepository.set(updatedAnswers) - } yield Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) - } + } yield Redirect(navigator.nextPage($className$Page, mode, updatedAnswers)) ) } } diff --git a/src/main/scaffolds/yesNoPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/yesNoPage/app/controllers/$className$Controller.scala index 7d9ea03b..736f4a22 100644 --- a/src/main/scaffolds/yesNoPage/app/controllers/$className$Controller.scala +++ b/src/main/scaffolds/yesNoPage/app/controllers/$className$Controller.scala @@ -3,10 +3,9 @@ package controllers import controllers.actions._ import forms.$className$FormProvider import javax.inject.Inject -import models.{Mode, UserAnswers} +import models.Mode import navigation.Navigator import pages.$className$Page -import play.api.data.Form import play.api.i18n.{I18nSupport, MessagesApi} import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} import repositories.SessionRepository @@ -27,7 +26,7 @@ class $className;format="cap"$Controller @Inject()( view: $className$View )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { - val form: Form[Boolean] = formProvider() + val form = formProvider() def onPageLoad(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData) { implicit request => @@ -40,19 +39,18 @@ class $className;format="cap"$Controller @Inject()( Ok(view(preparedForm, mode)) } - def onSubmit(mode: Mode) = (identify andThen getData andThen requireData).async { + def onSubmit(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData).async { implicit request => form.bindFromRequest().fold( - (formWithErrors: Form[_]) => + formWithErrors => Future.successful(BadRequest(view(formWithErrors, mode))), - value => { + value => for { updatedAnswers <- Future.fromTry(request.userAnswers.set($className$Page, value)) _ <- sessionRepository.set(updatedAnswers) - } yield Redirect(navigator.nextPage($className$Page, mode)(updatedAnswers)) - } + } yield Redirect(navigator.nextPage($className$Page, mode, updatedAnswers)) ) } } From fca3e0aed2973a23e624d7b4e0dfa4d3fc47e11e Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Tue, 4 Jun 2019 08:48:22 +0100 Subject: [PATCH 18/23] Stop application in controller tests --- .../g8/test/controllers/CheckYourAnswersControllerSpec.scala | 4 ++++ src/main/g8/test/controllers/IndexControllerSpec.scala | 2 ++ .../g8/test/controllers/SessionExpiredControllerSpec.scala | 2 ++ src/main/g8/test/controllers/UnauthorisedControllerSpec.scala | 2 ++ 4 files changed, 10 insertions(+) diff --git a/src/main/g8/test/controllers/CheckYourAnswersControllerSpec.scala b/src/main/g8/test/controllers/CheckYourAnswersControllerSpec.scala index 06b13f1d..42354bc3 100644 --- a/src/main/g8/test/controllers/CheckYourAnswersControllerSpec.scala +++ b/src/main/g8/test/controllers/CheckYourAnswersControllerSpec.scala @@ -24,6 +24,8 @@ class CheckYourAnswersControllerSpec extends SpecBase { contentAsString(result) mustEqual view(Seq(AnswerSection(None, Seq())))(fakeRequest, messages).toString + + application.stop() } "redirect to Session Expired for a GET if no existing data is found" in { @@ -37,6 +39,8 @@ class CheckYourAnswersControllerSpec extends SpecBase { status(result) mustEqual SEE_OTHER redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() } } } diff --git a/src/main/g8/test/controllers/IndexControllerSpec.scala b/src/main/g8/test/controllers/IndexControllerSpec.scala index 49e07b0f..f20bdbdd 100644 --- a/src/main/g8/test/controllers/IndexControllerSpec.scala +++ b/src/main/g8/test/controllers/IndexControllerSpec.scala @@ -23,6 +23,8 @@ class IndexControllerSpec extends SpecBase { contentAsString(result) mustEqual view()(fakeRequest, messages).toString + + application.stop() } } } diff --git a/src/main/g8/test/controllers/SessionExpiredControllerSpec.scala b/src/main/g8/test/controllers/SessionExpiredControllerSpec.scala index cfbd4873..97a0d8cd 100644 --- a/src/main/g8/test/controllers/SessionExpiredControllerSpec.scala +++ b/src/main/g8/test/controllers/SessionExpiredControllerSpec.scala @@ -23,6 +23,8 @@ class SessionExpiredControllerSpec extends SpecBase { contentAsString(result) mustEqual view()(fakeRequest, messages).toString + + application.stop() } } } diff --git a/src/main/g8/test/controllers/UnauthorisedControllerSpec.scala b/src/main/g8/test/controllers/UnauthorisedControllerSpec.scala index 75976172..e00db23f 100644 --- a/src/main/g8/test/controllers/UnauthorisedControllerSpec.scala +++ b/src/main/g8/test/controllers/UnauthorisedControllerSpec.scala @@ -23,6 +23,8 @@ class UnauthorisedControllerSpec extends SpecBase { contentAsString(result) mustEqual view()(fakeRequest, messages).toString + + application.stop() } } } From b40a70f345698923c9dcb6d3c519cc64f356a90e Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Mon, 29 Jul 2019 14:40:50 +0100 Subject: [PATCH 19/23] Add date page scaffold --- .../g8/app/forms/mappings/Constraints.scala | 18 + .../g8/app/forms/mappings/Formatters.scala | 7 +- .../forms/mappings/LocalDateFormatter.scala | 77 ++++ src/main/g8/app/forms/mappings/Mappings.scala | 10 + .../g8/app/utils/CheckYourAnswersHelper.scala | 8 + .../views/components/input_date.scala.html | 63 +--- .../forms/behaviours/DateBehaviours.scala | 84 +++++ .../test/forms/mappings/ConstraintsSpec.scala | 73 +++- .../forms/mappings/DateMappingsSpec.scala | 346 ++++++++++++++++++ src/main/g8/test/generators/Generators.scala | 20 +- .../controllers/$className$Controller.scala | 56 +++ .../app/forms/$className$FormProvider.scala | 20 + .../datePage/app/pages/$className$Page.scala | 12 + .../app/views/$className$View.scala.html | 28 ++ .../scaffolds/datePage/default.properties | 2 + .../$className$ControllerSpec.scala | 152 ++++++++ .../forms/$className$FormProviderSpec.scala | 23 ++ .../pages/$className$PageSpec.scala | 22 ++ .../views/$className$ViewSpec.scala | 31 ++ .../datePage/migrations/$className__snake$.sh | 66 ++++ 20 files changed, 1067 insertions(+), 51 deletions(-) create mode 100644 src/main/g8/app/forms/mappings/LocalDateFormatter.scala create mode 100644 src/main/g8/test/forms/behaviours/DateBehaviours.scala create mode 100644 src/main/g8/test/forms/mappings/DateMappingsSpec.scala create mode 100644 src/main/scaffolds/datePage/app/controllers/$className$Controller.scala create mode 100644 src/main/scaffolds/datePage/app/forms/$className$FormProvider.scala create mode 100644 src/main/scaffolds/datePage/app/pages/$className$Page.scala create mode 100644 src/main/scaffolds/datePage/app/views/$className$View.scala.html create mode 100644 src/main/scaffolds/datePage/default.properties create mode 100644 src/main/scaffolds/datePage/generated-test/controllers/$className$ControllerSpec.scala create mode 100644 src/main/scaffolds/datePage/generated-test/forms/$className$FormProviderSpec.scala create mode 100644 src/main/scaffolds/datePage/generated-test/pages/$className$PageSpec.scala create mode 100644 src/main/scaffolds/datePage/generated-test/views/$className$ViewSpec.scala create mode 100644 src/main/scaffolds/datePage/migrations/$className__snake$.sh diff --git a/src/main/g8/app/forms/mappings/Constraints.scala b/src/main/g8/app/forms/mappings/Constraints.scala index 1f7392c3..8c93a584 100644 --- a/src/main/g8/app/forms/mappings/Constraints.scala +++ b/src/main/g8/app/forms/mappings/Constraints.scala @@ -1,5 +1,7 @@ package forms.mappings +import java.time.LocalDate + import play.api.data.validation.{Constraint, Invalid, Valid} trait Constraints { @@ -67,4 +69,20 @@ trait Constraints { case _ => Invalid(errorKey, maximum) } + + protected def maxDate(maximum: LocalDate, errorKey: String, args: Any*): Constraint[LocalDate] = + Constraint { + case date if date.isAfter(maximum) => + Invalid(errorKey, args: _*) + case _ => + Valid + } + + protected def minDate(minimum: LocalDate, errorKey: String, args: Any*): Constraint[LocalDate] = + Constraint { + case date if date.isBefore(minimum) => + Invalid(errorKey, args: _*) + case _ => + Valid + } } diff --git a/src/main/g8/app/forms/mappings/Formatters.scala b/src/main/g8/app/forms/mappings/Formatters.scala index 2a00bb10..80efe824 100644 --- a/src/main/g8/app/forms/mappings/Formatters.scala +++ b/src/main/g8/app/forms/mappings/Formatters.scala @@ -37,7 +37,7 @@ trait Formatters { def unbind(key: String, value: Boolean) = Map(key -> value.toString) } - private[mappings] def intFormatter(requiredKey: String, wholeNumberKey: String, nonNumericKey: String): Formatter[Int] = + private[mappings] def intFormatter(requiredKey: String, wholeNumberKey: String, nonNumericKey: String, args: Seq[String] = Seq.empty): Formatter[Int] = new Formatter[Int] { val decimalRegexp = """^-?(\d*\.\d*)\$""" @@ -50,17 +50,18 @@ trait Formatters { .right.map(_.replace(",", "")) .right.flatMap { case s if s.matches(decimalRegexp) => - Left(Seq(FormError(key, wholeNumberKey))) + Left(Seq(FormError(key, wholeNumberKey, args))) case s => nonFatalCatch .either(s.toInt) - .left.map(_ => Seq(FormError(key, nonNumericKey))) + .left.map(_ => Seq(FormError(key, nonNumericKey, args))) } override def unbind(key: String, value: Int) = baseFormatter.unbind(key, value.toString) } + private[mappings] def enumerableFormatter[A](requiredKey: String, invalidKey: String)(implicit ev: Enumerable[A]): Formatter[A] = new Formatter[A] { diff --git a/src/main/g8/app/forms/mappings/LocalDateFormatter.scala b/src/main/g8/app/forms/mappings/LocalDateFormatter.scala new file mode 100644 index 00000000..eec14689 --- /dev/null +++ b/src/main/g8/app/forms/mappings/LocalDateFormatter.scala @@ -0,0 +1,77 @@ +package forms.mappings + +import java.time.LocalDate + +import play.api.data.FormError +import play.api.data.format.Formatter + +import scala.util.{Failure, Success, Try} + +private[mappings] class LocalDateFormatter( + invalidKey: String, + allRequiredKey: String, + twoRequiredKey: String, + requiredKey: String, + args: Seq[String] = Seq.empty + ) extends Formatter[LocalDate] with Formatters { + + private val fieldKeys: List[String] = List("day", "month", "year") + + private def toDate(key: String, day: Int, month: Int, year: Int): Either[Seq[FormError], LocalDate] = + Try(LocalDate.of(year, month, day)) match { + case Success(date) => + Right(date) + case Failure(_) => + Left(Seq(FormError(key, invalidKey, args))) + } + + private def formatDate(key: String, data: Map[String, String]): Either[Seq[FormError], LocalDate] = { + + val int = intFormatter( + requiredKey = invalidKey, + wholeNumberKey = invalidKey, + nonNumericKey = invalidKey, + args + ) + + for { + day <- int.bind(s"\$key.day", data).right + month <- int.bind(s"\$key.month", data).right + year <- int.bind(s"\$key.year", data).right + date <- toDate(key, day, month, year).right + } yield date + } + + override def bind(key: String, data: Map[String, String]): Either[Seq[FormError], LocalDate] = { + + val fields = fieldKeys.map { + field => + field -> data.get(s"\$key.\$field").filter(_.nonEmpty) + }.toMap + + lazy val missingFields = fields + .withFilter(_._2.isEmpty) + .map(_._1) + .toList + + fields.count(_._2.isDefined) match { + case 3 => + formatDate(key, data).left.map { + _.map(_.copy(key = key, args = args)) + } + case 2 => + Left(List(FormError(key, requiredKey, missingFields ++ args))) + case 1 => + Left(List(FormError(key, twoRequiredKey, missingFields ++ args))) + case _ => + Left(List(FormError(key, allRequiredKey, args))) + } + } + + override def unbind(key: String, value: LocalDate): Map[String, String] = + Map( + s"\$key.day" -> value.getDayOfMonth.toString, + s"\$key.month" -> value.getMonthValue.toString, + s"\$key.year" -> value.getYear.toString + ) +} diff --git a/src/main/g8/app/forms/mappings/Mappings.scala b/src/main/g8/app/forms/mappings/Mappings.scala index 3e0f78a4..d09e638c 100644 --- a/src/main/g8/app/forms/mappings/Mappings.scala +++ b/src/main/g8/app/forms/mappings/Mappings.scala @@ -1,5 +1,7 @@ package forms.mappings +import java.time.LocalDate + import play.api.data.FieldMapping import play.api.data.Forms.of import models.Enumerable @@ -22,4 +24,12 @@ trait Mappings extends Formatters with Constraints { protected def enumerable[A](requiredKey: String = "error.required", invalidKey: String = "error.invalid")(implicit ev: Enumerable[A]): FieldMapping[A] = of(enumerableFormatter[A](requiredKey, invalidKey)) + + protected def localDate( + invalidKey: String, + allRequiredKey: String, + twoRequiredKey: String, + requiredKey: String, + args: Seq[String] = Seq.empty): FieldMapping[LocalDate] = + of(new LocalDateFormatter(invalidKey, allRequiredKey, twoRequiredKey, requiredKey, args)) } diff --git a/src/main/g8/app/utils/CheckYourAnswersHelper.scala b/src/main/g8/app/utils/CheckYourAnswersHelper.scala index 54497fca..10895654 100644 --- a/src/main/g8/app/utils/CheckYourAnswersHelper.scala +++ b/src/main/g8/app/utils/CheckYourAnswersHelper.scala @@ -1,11 +1,14 @@ package utils +import java.time.format.DateTimeFormatter + import controllers.routes import models.{CheckMode, UserAnswers} import pages._ import play.api.i18n.Messages import play.twirl.api.{Html, HtmlFormat} import viewmodels.AnswerRow +import CheckYourAnswersHelper._ class CheckYourAnswersHelper(userAnswers: UserAnswers)(implicit messages: Messages) { @@ -16,3 +19,8 @@ class CheckYourAnswersHelper(userAnswers: UserAnswers)(implicit messages: Messag HtmlFormat.escape(messages("site.no")) } } + +object CheckYourAnswersHelper { + + private val dateFormatter = DateTimeFormatter.ofPattern("d MMMM yyyy") +} diff --git a/src/main/g8/app/views/components/input_date.scala.html b/src/main/g8/app/views/components/input_date.scala.html index f03917de..9623636b 100644 --- a/src/main/g8/app/views/components/input_date.scala.html +++ b/src/main/g8/app/views/components/input_date.scala.html @@ -1,53 +1,30 @@ @( -id: String, -label: String, -legendClass: String = "", -errorKey: String, -dayErrorKey: String, -monthErrorKey: String, -yearErrorKey: String, -valueDay: Option[String] = None, -valueMonth: Option[String] = None, -valueYear: Option[String] = None, -secondaryLabel: Option[String] = None, -hint: Option[String] = None + field: Field, + legend: String, + legendClass: String = "", + hint: Option[String] = None )(implicit messages: Messages) -
    +
    - - @label - - @if(hint.nonEmpty){ - @hint - } - @if(errorKey.nonEmpty){ - @messages(errorKey) - } - @if(dayErrorKey.nonEmpty){ - @messages(dayErrorKey) - } - @if(monthErrorKey.nonEmpty){ - @messages(monthErrorKey) - } - @if(yearErrorKey.nonEmpty){ - @messages(yearErrorKey) + + @legend + + @if(hint.nonEmpty){ + @hint + } + @field.errors.map { error => + @messages(error.message, error.args: _*) } -
    -
    - - -
    -
    - - -
    -
    - - -
    +
    + @List("day", "month", "year").map { part => +
    + + +
    + }
    diff --git a/src/main/g8/test/forms/behaviours/DateBehaviours.scala b/src/main/g8/test/forms/behaviours/DateBehaviours.scala new file mode 100644 index 00000000..af35fc40 --- /dev/null +++ b/src/main/g8/test/forms/behaviours/DateBehaviours.scala @@ -0,0 +1,84 @@ +package forms.behaviours + +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +import org.scalacheck.Gen +import play.api.data.{Form, FormError} + +class DateBehaviours extends FieldBehaviours { + + def dateField(form: Form[_], key: String, validData: Gen[LocalDate]): Unit = { + + "bind valid data" in { + + forAll(validData -> "valid date") { + date => + + val data = Map( + s"\$key.day" -> date.getDayOfMonth.toString, + s"\$key.month" -> date.getMonthValue.toString, + s"\$key.year" -> date.getYear.toString + ) + + val result = form.bind(data) + + result.value.value shouldEqual date + } + } + } + + def dateFieldWithMax(form: Form[_], key: String, max: LocalDate, formError: FormError): Unit = { + + s"fail to bind a date greater than \${max.format(DateTimeFormatter.ISO_LOCAL_DATE)}" in { + + val generator = datesBetween(max.plusDays(1), max.plusYears(10)) + + forAll(generator -> "invalid dates") { + date => + + val data = Map( + s"\$key.day" -> date.getDayOfMonth.toString, + s"\$key.month" -> date.getMonthValue.toString, + s"\$key.year" -> date.getYear.toString + ) + + val result = form.bind(data) + + result.errors should contain only formError + } + } + } + + def dateFieldWithMin(form: Form[_], key: String, min: LocalDate, formError: FormError): Unit = { + + s"fail to bind a date earlier than \${min.format(DateTimeFormatter.ISO_LOCAL_DATE)}" in { + + val generator = datesBetween(min.minusYears(10), min.minusDays(1)) + + forAll(generator -> "invalid dates") { + date => + + val data = Map( + s"\$key.day" -> date.getDayOfMonth.toString, + s"\$key.month" -> date.getMonthValue.toString, + s"\$key.year" -> date.getYear.toString + ) + + val result = form.bind(data) + + result.errors should contain only formError + } + } + } + + def mandatoryDateField(form: Form[_], key: String, requiredAllKey: String, errorArgs: Seq[String] = Seq.empty): Unit = { + + "fail to bind an empty date" in { + + val result = form.bind(Map.empty[String, String]) + + result.errors should contain only FormError(key, requiredAllKey, errorArgs) + } + } +} diff --git a/src/main/g8/test/forms/mappings/ConstraintsSpec.scala b/src/main/g8/test/forms/mappings/ConstraintsSpec.scala index a06875f4..911a7035 100644 --- a/src/main/g8/test/forms/mappings/ConstraintsSpec.scala +++ b/src/main/g8/test/forms/mappings/ConstraintsSpec.scala @@ -1,9 +1,14 @@ package forms.mappings +import java.time.LocalDate + +import generators.Generators +import org.scalacheck.Gen +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks import org.scalatest.{MustMatchers, WordSpec} import play.api.data.validation.{Invalid, Valid} -class ConstraintsSpec extends WordSpec with MustMatchers with Constraints { +class ConstraintsSpec extends WordSpec with MustMatchers with ScalaCheckPropertyChecks with Generators with Constraints { "firstError" must { @@ -100,4 +105,70 @@ class ConstraintsSpec extends WordSpec with MustMatchers with Constraints { result mustEqual Invalid("error.length", 10) } } + + "maxDate" must { + + "return Valid for a date before or equal to the maximum" in { + + val gen: Gen[(LocalDate, LocalDate)] = for { + max <- datesBetween(LocalDate.of(2000, 1, 1), LocalDate.of(3000, 1, 1)) + date <- datesBetween(LocalDate.of(2000, 1, 1), max) + } yield (max, date) + + forAll(gen) { + case (max, date) => + + val result = maxDate(max, "error.future")(date) + result mustEqual Valid + } + } + + "return Invalid for a date after the maximum" in { + + val gen: Gen[(LocalDate, LocalDate)] = for { + max <- datesBetween(LocalDate.of(2000, 1, 1), LocalDate.of(3000, 1, 1)) + date <- datesBetween(max.plusDays(1), LocalDate.of(3000, 1, 2)) + } yield (max, date) + + forAll(gen) { + case (max, date) => + + val result = maxDate(max, "error.future", "foo")(date) + result mustEqual Invalid("error.future", "foo") + } + } + } + + "minDate" must { + + "return Valid for a date after or equal to the minimum" in { + + val gen: Gen[(LocalDate, LocalDate)] = for { + min <- datesBetween(LocalDate.of(2000, 1, 1), LocalDate.of(3000, 1, 1)) + date <- datesBetween(min, LocalDate.of(3000, 1, 1)) + } yield (min, date) + + forAll(gen) { + case (min, date) => + + val result = minDate(min, "error.past", "foo")(date) + result mustEqual Valid + } + } + + "return Invalid for a date before the minimum" in { + + val gen: Gen[(LocalDate, LocalDate)] = for { + min <- datesBetween(LocalDate.of(2000, 1, 2), LocalDate.of(3000, 1, 1)) + date <- datesBetween(LocalDate.of(2000, 1, 1), min.minusDays(1)) + } yield (min, date) + + forAll(gen) { + case (min, date) => + + val result = minDate(min, "error.past", "foo")(date) + result mustEqual Invalid("error.past", "foo") + } + } + } } diff --git a/src/main/g8/test/forms/mappings/DateMappingsSpec.scala b/src/main/g8/test/forms/mappings/DateMappingsSpec.scala new file mode 100644 index 00000000..3c20326c --- /dev/null +++ b/src/main/g8/test/forms/mappings/DateMappingsSpec.scala @@ -0,0 +1,346 @@ +package forms.mappings + +import java.time.LocalDate + +import generators.Generators +import org.scalacheck.Gen +import org.scalatest.{FreeSpec, MustMatchers, OptionValues} +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks +import play.api.data.{Form, FormError} + +class DateMappingsSpec extends FreeSpec with MustMatchers with ScalaCheckPropertyChecks with Generators with OptionValues + with Mappings { + + val form = Form( + "value" -> localDate( + requiredKey = "error.required", + allRequiredKey = "error.required.all", + twoRequiredKey = "error.required.two", + invalidKey = "error.invalid" + ) + ) + + val validData = datesBetween( + min = LocalDate.of(2000, 1, 1), + max = LocalDate.of(3000, 1, 1) + ) + + val invalidField: Gen[String] = Gen.alphaStr.suchThat(_.nonEmpty) + + val missingField: Gen[Option[String]] = Gen.option(Gen.const("")) + + "bind valid data" in { + + forAll(validData -> "valid date") { + date => + + val data = Map( + "value.day" -> date.getDayOfMonth.toString, + "value.month" -> date.getMonthValue.toString, + "value.year" -> date.getYear.toString + ) + + val result = form.bind(data) + + result.value.value mustEqual date + } + } + + "fail to bind an empty date" in { + + val result = form.bind(Map.empty[String, String]) + + result.errors must contain only FormError("value", "error.required.all", List.empty) + } + + "fail to bind a date with a missing day" in { + + forAll(validData -> "valid date", missingField -> "missing field") { + (date, field) => + + val initialData = Map( + "value.month" -> date.getMonthValue.toString, + "value.year" -> date.getYear.toString + ) + + val data = field.fold(initialData) { + value => + initialData + ("value.day" -> value) + } + + val result = form.bind(data) + + result.errors must contain only FormError("value", "error.required", List("day")) + } + } + + "fail to bind a date with an invalid day" in { + + forAll(validData -> "valid date", invalidField -> "invalid field") { + (date, field) => + + val data = Map( + "value.day" -> field, + "value.month" -> date.getMonthValue.toString, + "value.year" -> date.getYear.toString + ) + + val result = form.bind(data) + + result.errors must contain( + FormError("value", "error.invalid", List.empty) + ) + } + } + + "fail to bind a date with a missing month" in { + + forAll(validData -> "valid date", missingField -> "missing field") { + (date, field) => + + val initialData = Map( + "value.day" -> date.getDayOfMonth.toString, + "value.year" -> date.getYear.toString + ) + + val data = field.fold(initialData) { + value => + initialData + ("value.month" -> value) + } + + val result = form.bind(data) + + result.errors must contain only FormError("value", "error.required", List("month")) + } + } + + "fail to bind a date with an invalid month" in { + + forAll(validData -> "valid data", invalidField -> "invalid field") { + (date, field) => + + val data = Map( + "value.day" -> date.getDayOfMonth.toString, + "value.month" -> field, + "value.year" -> date.getYear.toString + ) + + val result = form.bind(data) + + result.errors must contain( + FormError("value", "error.invalid", List.empty) + ) + } + } + + "fail to bind a date with a missing year" in { + + forAll(validData -> "valid date", missingField -> "missing field") { + (date, field) => + + val initialData = Map( + "value.day" -> date.getDayOfMonth.toString, + "value.month" -> date.getMonthValue.toString + ) + + val data = field.fold(initialData) { + value => + initialData + ("value.year" -> value) + } + + val result = form.bind(data) + + result.errors must contain only FormError("value", "error.required", List("year")) + } + } + + "fail to bind a date with an invalid year" in { + + forAll(validData -> "valid data", invalidField -> "invalid field") { + (date, field) => + + val data = Map( + "value.day" -> date.getDayOfMonth.toString, + "value.month" -> date.getMonthValue.toString, + "value.year" -> field + ) + + val result = form.bind(data) + + result.errors must contain( + FormError("value", "error.invalid", List.empty) + ) + } + } + + "fail to bind a date with a missing day and month" in { + + forAll(validData -> "valid date", missingField -> "missing day", missingField -> "missing month") { + (date, dayOpt, monthOpt) => + + val day = dayOpt.fold(Map.empty[String, String]) { + value => + Map("value.day" -> value) + } + + val month = monthOpt.fold(Map.empty[String, String]) { + value => + Map("value.month" -> value) + } + + val data: Map[String, String] = Map( + "value.year" -> date.getYear.toString + ) ++ day ++ month + + val result = form.bind(data) + + result.errors must contain only FormError("value", "error.required.two", List("day", "month")) + } + } + + "fail to bind a date with a missing day and year" in { + + forAll(validData -> "valid date", missingField -> "missing day", missingField -> "missing year") { + (date, dayOpt, yearOpt) => + + val day = dayOpt.fold(Map.empty[String, String]) { + value => + Map("value.day" -> value) + } + + val year = yearOpt.fold(Map.empty[String, String]) { + value => + Map("value.year" -> value) + } + + val data: Map[String, String] = Map( + "value.month" -> date.getMonthValue.toString + ) ++ day ++ year + + val result = form.bind(data) + + result.errors must contain only FormError("value", "error.required.two", List("day", "year")) + } + } + + "fail to bind a date with a missing month and year" in { + + forAll(validData -> "valid date", missingField -> "missing month", missingField -> "missing year") { + (date, monthOpt, yearOpt) => + + val month = monthOpt.fold(Map.empty[String, String]) { + value => + Map("value.month" -> value) + } + + val year = yearOpt.fold(Map.empty[String, String]) { + value => + Map("value.year" -> value) + } + + val data: Map[String, String] = Map( + "value.day" -> date.getDayOfMonth.toString + ) ++ month ++ year + + val result = form.bind(data) + + result.errors must contain only FormError("value", "error.required.two", List("month", "year")) + } + } + + "fail to bind an invalid day and month" in { + + forAll(validData -> "valid date", invalidField -> "invalid day", invalidField -> "invalid month") { + (date, day, month) => + + val data = Map( + "value.day" -> day, + "value.month" -> month, + "value.year" -> date.getYear.toString + ) + + val result = form.bind(data) + + result.errors must contain only FormError("value", "error.invalid", List.empty) + } + } + + "fail to bind an invalid day and year" in { + + forAll(validData -> "valid date", invalidField -> "invalid day", invalidField -> "invalid year") { + (date, day, year) => + + val data = Map( + "value.day" -> day, + "value.month" -> date.getMonthValue.toString, + "value.year" -> year + ) + + val result = form.bind(data) + + result.errors must contain only FormError("value", "error.invalid", List.empty) + } + } + + "fail to bind an invalid month and year" in { + + forAll(validData -> "valid date", invalidField -> "invalid month", invalidField -> "invalid year") { + (date, month, year) => + + val data = Map( + "value.day" -> date.getDayOfMonth.toString, + "value.month" -> month, + "value.year" -> year + ) + + val result = form.bind(data) + + result.errors must contain only FormError("value", "error.invalid", List.empty) + } + } + + "fail to bind an invalid day, month and year" in { + + forAll(invalidField -> "valid day", invalidField -> "invalid month", invalidField -> "invalid year") { + (day, month, year) => + + val data = Map( + "value.day" -> day, + "value.month" -> month, + "value.year" -> year + ) + + val result = form.bind(data) + + result.errors must contain only FormError("value", "error.invalid", List.empty) + } + } + + "fail to bind an invalid date" in { + + val data = Map( + "value.day" -> "30", + "value.month" -> "2", + "value.year" -> "2018" + ) + + val result = form.bind(data) + + result.errors must contain( + FormError("value", "error.invalid", List.empty) + ) + } + + "unbind a date" in { + + forAll(validData -> "valid date") { + date => + + val filledForm = form.fill(date) + + filledForm("value.day").value.value mustEqual date.getDayOfMonth.toString + filledForm("value.month").value.value mustEqual date.getMonthValue.toString + filledForm("value.year").value.value mustEqual date.getYear.toString + } + } +} diff --git a/src/main/g8/test/generators/Generators.scala b/src/main/g8/test/generators/Generators.scala index eb40b5c9..f549ae31 100644 --- a/src/main/g8/test/generators/Generators.scala +++ b/src/main/g8/test/generators/Generators.scala @@ -1,9 +1,10 @@ package generators -import org.scalacheck.{Arbitrary, Gen, Shrink} -import Gen._ -import Arbitrary._ -import play.api.libs.json.{JsBoolean, JsNumber, JsString} +import java.time.{Instant, LocalDate, ZoneOffset} + +import org.scalacheck.Arbitrary._ +import org.scalacheck.Gen._ +import org.scalacheck.{Gen, Shrink} trait Generators extends UserAnswersGenerator with PageGenerators with ModelGenerators with UserAnswersEntryGenerators { @@ -89,4 +90,15 @@ trait Generators extends UserAnswersGenerator with PageGenerators with ModelGene val vector = xs.toVector choose(0, vector.size - 1).flatMap(vector(_)) } + + def datesBetween(min: LocalDate, max: LocalDate): Gen[LocalDate] = { + + def toMillis(date: LocalDate): Long = + date.atStartOfDay.atZone(ZoneOffset.UTC).toInstant.toEpochMilli + + Gen.choose(toMillis(min), toMillis(max)).map { + millis => + Instant.ofEpochMilli(millis).atOffset(ZoneOffset.UTC).toLocalDate + } + } } diff --git a/src/main/scaffolds/datePage/app/controllers/$className$Controller.scala b/src/main/scaffolds/datePage/app/controllers/$className$Controller.scala new file mode 100644 index 00000000..8aa6e555 --- /dev/null +++ b/src/main/scaffolds/datePage/app/controllers/$className$Controller.scala @@ -0,0 +1,56 @@ +package controllers + +import controllers.actions._ +import forms.$className$FormProvider +import javax.inject.Inject +import models.Mode +import navigation.Navigator +import pages.$className$Page +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import repositories.SessionRepository +import uk.gov.hmrc.play.bootstrap.controller.FrontendBaseController +import views.html.$className$View + +import scala.concurrent.{ExecutionContext, Future} + +class $className$Controller @Inject()( + override val messagesApi: MessagesApi, + sessionRepository: SessionRepository, + navigator: Navigator, + identify: IdentifierAction, + getData: DataRetrievalAction, + requireData: DataRequiredAction, + formProvider: $className$FormProvider, + val controllerComponents: MessagesControllerComponents, + view: $className$View + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { + + val form = formProvider() + + def onPageLoad(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData) { + implicit request => + + val preparedForm = request.userAnswers.get($className$Page) match { + case None => form + case Some(value) => form.fill(value) + } + + Ok(view(preparedForm, mode)) + } + + def onSubmit(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData).async { + implicit request => + + form.bindFromRequest().fold( + formWithErrors => + Future.successful(BadRequest(view(formWithErrors, mode))), + + value => + for { + updatedAnswers <- Future.fromTry(request.userAnswers.set($className$Page, value)) + _ <- sessionRepository.set(updatedAnswers) + } yield Redirect(navigator.nextPage($className$Page, mode, updatedAnswers)) + ) + } +} diff --git a/src/main/scaffolds/datePage/app/forms/$className$FormProvider.scala b/src/main/scaffolds/datePage/app/forms/$className$FormProvider.scala new file mode 100644 index 00000000..e76be781 --- /dev/null +++ b/src/main/scaffolds/datePage/app/forms/$className$FormProvider.scala @@ -0,0 +1,20 @@ +package forms + +import java.time.LocalDate + +import forms.mappings.Mappings +import javax.inject.Inject +import play.api.data.Form + +class $className$FormProvider @Inject() extends Mappings { + + def apply(): Form[LocalDate] = + Form( + "value" -> localDate( + invalidKey = "$className;format="decap"$.error.invalid", + allRequiredKey = "$className;format="decap"$.error.required.all", + twoRequiredKey = "$className;format="decap"$.error.required.two", + requiredKey = "$className;format="decap"$.error.required" + ) + ) +} diff --git a/src/main/scaffolds/datePage/app/pages/$className$Page.scala b/src/main/scaffolds/datePage/app/pages/$className$Page.scala new file mode 100644 index 00000000..222c3af9 --- /dev/null +++ b/src/main/scaffolds/datePage/app/pages/$className$Page.scala @@ -0,0 +1,12 @@ +package pages + +import java.time.LocalDate + +import play.api.libs.json.JsPath + +case object $className$Page extends QuestionPage[LocalDate] { + + override def path: JsPath = JsPath \ toString + + override def toString: String = "$className;format="decap"$" +} diff --git a/src/main/scaffolds/datePage/app/views/$className$View.scala.html b/src/main/scaffolds/datePage/app/views/$className$View.scala.html new file mode 100644 index 00000000..a0f0254a --- /dev/null +++ b/src/main/scaffolds/datePage/app/views/$className$View.scala.html @@ -0,0 +1,28 @@ +@this( + main_template: MainTemplate, + formHelper: FormWithCSRF +) + +@(form: Form[_], mode: Mode)(implicit request: Request[_], messages: Messages) + +@main_template( + title = s"\${errorPrefix(form)} \${messages("$className;format="decap"$.title")}" +) { + + @formHelper(action = $className$Controller.onSubmit(mode), 'autoComplete -> "off") { + + @components.back_link() + + @components.error_summary(form.errors) + + @components.heading("$className;format="decap"$.heading") + + @components.input_date( + field = form("value"), + legend = messages("$className;format="decap"$.heading"), + legendClass = "visually-hidden" + ) + + @components.submit_button() + } +} diff --git a/src/main/scaffolds/datePage/default.properties b/src/main/scaffolds/datePage/default.properties new file mode 100644 index 00000000..d73a936d --- /dev/null +++ b/src/main/scaffolds/datePage/default.properties @@ -0,0 +1,2 @@ +description = Generates a controller and view for a page with a single date +className = MyNewDatePage diff --git a/src/main/scaffolds/datePage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/datePage/generated-test/controllers/$className$ControllerSpec.scala new file mode 100644 index 00000000..5f2d4213 --- /dev/null +++ b/src/main/scaffolds/datePage/generated-test/controllers/$className$ControllerSpec.scala @@ -0,0 +1,152 @@ +package controllers + +import java.time.{LocalDate, ZoneOffset} + +import base.SpecBase +import forms.$className$FormProvider +import models.{NormalMode, UserAnswers} +import navigation.{FakeNavigator, Navigator} +import org.mockito.Matchers.any +import org.mockito.Mockito.when +import org.scalatest.mockito.MockitoSugar +import pages.$className$Page +import play.api.inject.bind +import play.api.mvc.{AnyContentAsEmpty, AnyContentAsFormUrlEncoded, Call} +import play.api.test.FakeRequest +import play.api.test.Helpers._ +import repositories.SessionRepository +import views.html.$className$View + +import scala.concurrent.Future + +class $className$ControllerSpec extends SpecBase with MockitoSugar { + + val formProvider = new $className$FormProvider() + private def form = formProvider() + + def onwardRoute = Call("GET", "/foo") + + val validAnswer = LocalDate.now(ZoneOffset.UTC) + + lazy val $className;format="decap"$Route = routes.$className$Controller.onPageLoad(NormalMode).url + + override val emptyUserAnswers = UserAnswers(userAnswersId) + + def getRequest(): FakeRequest[AnyContentAsEmpty.type] = + FakeRequest(GET, $className;format="decap"$Route) + + def postRequest(): FakeRequest[AnyContentAsFormUrlEncoded] = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody( + "value.day" -> validAnswer.getDayOfMonth.toString, + "value.month" -> validAnswer.getMonthValue.toString, + "value.year" -> validAnswer.getYear.toString + ) + + "$className$ Controller" must { + + "return OK and the correct view for a GET" in { + + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() + + val result = route(application, getRequest).value + + val view = application.injector.instanceOf[$className$View] + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view(form, NormalMode)(fakeRequest, messages).toString + + application.stop() + } + + "populate the view correctly on a GET when the question has previously been answered" in { + + val userAnswers = UserAnswers(userAnswersId).set($className$Page, validAnswer).success.value + + val application = applicationBuilder(userAnswers = Some(userAnswers)).build() + + val view = application.injector.instanceOf[$className$View] + + val result = route(application, getRequest).value + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view(form.fill(validAnswer), NormalMode)(getRequest, messages).toString + + application.stop() + } + + "redirect to the next page when valid data is submitted" in { + + val mockSessionRepository = mock[SessionRepository] + + when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + + val application = + applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides( + bind[Navigator].toInstance(new FakeNavigator(onwardRoute)), + bind[SessionRepository].toInstance(mockSessionRepository) + ) + .build() + + val result = route(application, postRequest).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual onwardRoute.url + + application.stop() + } + + "return a Bad Request and errors when invalid data is submitted" in { + + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", "invalid value")) + + val boundForm = form.bind(Map("value" -> "invalid value")) + + val view = application.injector.instanceOf[$className$View] + + val result = route(application, request).value + + status(result) mustEqual BAD_REQUEST + + contentAsString(result) mustEqual + view(boundForm, NormalMode)(fakeRequest, messages).toString + + application.stop() + } + + "redirect to Session Expired for a GET if no existing data is found" in { + + val application = applicationBuilder(userAnswers = None).build() + + val result = route(application, getRequest).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() + } + + "redirect to Session Expired for a POST if no existing data is found" in { + + val application = applicationBuilder(userAnswers = None).build() + + val result = route(application, postRequest).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() + } + } +} diff --git a/src/main/scaffolds/datePage/generated-test/forms/$className$FormProviderSpec.scala b/src/main/scaffolds/datePage/generated-test/forms/$className$FormProviderSpec.scala new file mode 100644 index 00000000..38e34336 --- /dev/null +++ b/src/main/scaffolds/datePage/generated-test/forms/$className$FormProviderSpec.scala @@ -0,0 +1,23 @@ +package forms + +import java.time.{LocalDate, ZoneOffset} + +import forms.behaviours.DateBehaviours +import play.api.data.FormError + +class $className$FormProviderSpec extends DateBehaviours { + + val form = new $className$FormProvider()() + + ".value" should { + + val validData = datesBetween( + min = LocalDate.of(2000, 1, 1), + max = LocalDate.now(ZoneOffset.UTC) + ) + + behave like dateField(form, "value", validData) + + behave like mandatoryDateField(form, "value", "$className;format="decap"$.error.required.all") + } +} diff --git a/src/main/scaffolds/datePage/generated-test/pages/$className$PageSpec.scala b/src/main/scaffolds/datePage/generated-test/pages/$className$PageSpec.scala new file mode 100644 index 00000000..19b4db81 --- /dev/null +++ b/src/main/scaffolds/datePage/generated-test/pages/$className$PageSpec.scala @@ -0,0 +1,22 @@ +package pages + +import java.time.LocalDate + +import org.scalacheck.Arbitrary +import pages.behaviours.PageBehaviours + +class $className$PageSpec extends PageBehaviours { + + "$className$Page" must { + + implicit lazy val arbitraryLocalDate: Arbitrary[LocalDate] = Arbitrary { + datesBetween(LocalDate.of(1900, 1, 1), LocalDate.of(2100, 1, 1)) + } + + beRetrievable[LocalDate]($className$Page) + + beSettable[LocalDate]($className$Page) + + beRemovable[LocalDate]($className$Page) + } +} diff --git a/src/main/scaffolds/datePage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/datePage/generated-test/views/$className$ViewSpec.scala new file mode 100644 index 00000000..595f4de0 --- /dev/null +++ b/src/main/scaffolds/datePage/generated-test/views/$className$ViewSpec.scala @@ -0,0 +1,31 @@ +package views + +import java.time.LocalDate + +import forms.$className$FormProvider +import models.{NormalMode, UserAnswers} +import play.api.data.Form +import play.twirl.api.HtmlFormat +import views.behaviours.QuestionViewBehaviours +import views.html.$className$View + +class $className$ViewSpec extends QuestionViewBehaviours[LocalDate] { + + val messageKeyPrefix = "$className;format="decap"$" + + val form = new $className$FormProvider()() + + "$className$View view" must { + + val application = applicationBuilder(userAnswers = Some(UserAnswers(userAnswersId))).build() + + val view = application.injector.instanceOf[$className$View] + + def applyView(form: Form[_]): HtmlFormat.Appendable = + view.apply(form, NormalMode)(fakeRequest, messages) + + behave like normalPage(applyView(form), messageKeyPrefix) + + behave like pageWithBackLink(applyView(form)) + } +} diff --git a/src/main/scaffolds/datePage/migrations/$className__snake$.sh b/src/main/scaffolds/datePage/migrations/$className__snake$.sh new file mode 100644 index 00000000..98670a56 --- /dev/null +++ b/src/main/scaffolds/datePage/migrations/$className__snake$.sh @@ -0,0 +1,66 @@ +#!/bin/bash + +echo "" +echo "Applying migration $className;format="snake"$" + +echo "Adding routes to conf/app.routes" + +echo "" >> ../conf/app.routes +echo "GET /$className;format="decap"$ controllers.$className$Controller.onPageLoad(mode: Mode = NormalMode)" >> ../conf/app.routes +echo "POST /$className;format="decap"$ controllers.$className$Controller.onSubmit(mode: Mode = NormalMode)" >> ../conf/app.routes + +echo "GET /change$className$ controllers.$className$Controller.onPageLoad(mode: Mode = CheckMode)" >> ../conf/app.routes +echo "POST /change$className$ controllers.$className$Controller.onSubmit(mode: Mode = CheckMode)" >> ../conf/app.routes + +echo "Adding messages to conf.messages" +echo "" >> ../conf/messages.en +echo "$className;format="decap"$.title = $className$" >> ../conf/messages.en +echo "$className;format="decap"$.heading = $className$" >> ../conf/messages.en +echo "$className;format="decap"$.checkYourAnswersLabel = $className$" >> ../conf/messages.en +echo "$className;format="decap"$.error.required.all = Enter the $className;format="decap"$" >> ../conf/messages.en +echo "$className;format="decap"$.error.required.two = The $className;format="decap"$" must include {0} and {1} >> ../conf/messages.en +echo "$className;format="decap"$.error.required = The $className;format="decap"$ must include {0}" >> ../conf/messages.en +echo "$className;format="decap"$.error.invalid = Enter a real $className$" >> ../conf/messages.en + +echo "Adding to UserAnswersEntryGenerators" +awk '/trait UserAnswersEntryGenerators/ {\ + print;\ + print "";\ + print " implicit lazy val arbitrary$className$UserAnswersEntry: Arbitrary[($className$Page.type, JsValue)] =";\ + print " Arbitrary {";\ + print " for {";\ + print " page <- arbitrary[$className$Page.type]";\ + print " value <- arbitrary[Int].map(Json.toJson(_))";\ + print " } yield (page, value)";\ + print " }";\ + next }1' ../test/generators/UserAnswersEntryGenerators.scala > tmp && mv tmp ../test/generators/UserAnswersEntryGenerators.scala + +echo "Adding to PageGenerators" +awk '/trait PageGenerators/ {\ + print;\ + print "";\ + print " implicit lazy val arbitrary$className$Page: Arbitrary[$className$Page.type] =";\ + print " Arbitrary($className$Page)";\ + next }1' ../test/generators/PageGenerators.scala > tmp && mv tmp ../test/generators/PageGenerators.scala + +echo "Adding to UserAnswersGenerator" +awk '/val generators/ {\ + print;\ + print " arbitrary[($className$Page.type, JsValue)] ::";\ + next }1' ../test/generators/UserAnswersGenerator.scala > tmp && mv tmp ../test/generators/UserAnswersGenerator.scala + +echo "Adding helper method to CheckYourAnswersHelper" +awk '/class CheckYourAnswersHelper/ {\ + print;\ + print "";\ + print " def $className;format="decap"$: Option[AnswerRow] = userAnswers.get($className$Page) map {";\ + print " x =>";\ + print " AnswerRow(";\ + print " HtmlFormat.escape(messages(\"$className;format="decap"$.checkYourAnswersLabel\")),";\ + print " HtmlFormat.escape(x.format(dateFormatter)),";\ + print " routes.$className$Controller.onPageLoad(CheckMode).url";\ + print " )";\ + print " }";\ + next }1' ../app/utils/CheckYourAnswersHelper.scala > tmp && mv tmp ../app/utils/CheckYourAnswersHelper.scala + +echo "Migration $className;format="snake"$ completed" From 78defb50d2e881865f7edf942a8bbcb6f7cfaa3f Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Thu, 1 Aug 2019 08:27:47 +0100 Subject: [PATCH 20/23] Update libraries --- src/main/g8/build.sbt | 1 - src/main/g8/project/AppDependencies.scala | 12 ++++-------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/g8/build.sbt b/src/main/g8/build.sbt index c46439af..6f04308a 100644 --- a/src/main/g8/build.sbt +++ b/src/main/g8/build.sbt @@ -34,7 +34,6 @@ lazy val root = (project in file(".")) ScoverageKeys.coverageHighlighting := true, scalacOptions ++= Seq("-feature"), libraryDependencies ++= AppDependencies(), - dependencyOverrides ++= AppDependencies.overrides, retrieveManaged := true, evictionWarningOptions in update := EvictionWarningOptions.default.withWarnScalaVersionEviction(false), diff --git a/src/main/g8/project/AppDependencies.scala b/src/main/g8/project/AppDependencies.scala index 6971f7ec..d495f7a7 100644 --- a/src/main/g8/project/AppDependencies.scala +++ b/src/main/g8/project/AppDependencies.scala @@ -5,13 +5,13 @@ object AppDependencies { val compile = Seq( play.sbt.PlayImport.ws, - "org.reactivemongo" %% "play2-reactivemongo" % "0.16.0-play26", + "org.reactivemongo" %% "play2-reactivemongo" % "0.18.3-play26", "uk.gov.hmrc" %% "logback-json-logger" % "3.1.0", - "uk.gov.hmrc" %% "govuk-template" % "5.35.0-play-26", + "uk.gov.hmrc" %% "govuk-template" % "5.36.0-play-26", "uk.gov.hmrc" %% "play-health" % "3.14.0-play-26", - "uk.gov.hmrc" %% "play-ui" % "7.39.0-play-26", + "uk.gov.hmrc" %% "play-ui" % "7.40.0-play-26", "uk.gov.hmrc" %% "play-conditional-form-mapping" % "0.2.0", - "uk.gov.hmrc" %% "bootstrap-play-26" % "0.39.0", + "uk.gov.hmrc" %% "bootstrap-play-26" % "0.42.0", "uk.gov.hmrc" %% "play-whitelist-filter" % "2.0.0" ) @@ -25,9 +25,5 @@ object AppDependencies { "org.scalacheck" %% "scalacheck" % "1.14.0" ).map(_ % Test) - val overrides: Set[ModuleID] = Set( - "org.reactivemongo" %% "reactivemongo" % "0.16.1" - ) - def apply(): Seq[ModuleID] = compile ++ test } From 7198fb2e1dfa03684aee82a8930ae27ace075bd5 Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Tue, 30 Jul 2019 10:13:48 +0100 Subject: [PATCH 21/23] Add checkbox page --- .../g8/app/forms/mappings/Constraints.scala | 8 + .../components/input_checkboxes.scala.html | 66 ++++++++ .../behaviours/CheckboxFieldBehaviours.scala | 45 ++++++ .../behaviours/CheckboxViewBehaviours.scala | 96 +++++++++++ .../controllers/$className$Controller.scala | 56 +++++++ .../app/forms/$className$FormProvider.scala | 16 ++ .../checkboxPage/app/models/$className$.scala | 23 +++ .../app/pages/$className$Page.scala | 11 ++ .../app/views/$className$View.scala.html | 32 ++++ .../scaffolds/checkboxPage/default.properties | 7 + .../$className$ControllerSpec.scala | 150 ++++++++++++++++++ .../forms/$className$FormProviderSpec.scala | 29 ++++ .../models/$className$Spec.scala | 47 ++++++ .../pages/$className$PageSpec.scala | 16 ++ .../views/$className$ViewSpec.scala | 30 ++++ .../migrations/$className__snake$.sh | 75 +++++++++ 16 files changed, 707 insertions(+) create mode 100644 src/main/g8/app/views/components/input_checkboxes.scala.html create mode 100644 src/main/g8/test/forms/behaviours/CheckboxFieldBehaviours.scala create mode 100644 src/main/g8/test/views/behaviours/CheckboxViewBehaviours.scala create mode 100644 src/main/scaffolds/checkboxPage/app/controllers/$className$Controller.scala create mode 100644 src/main/scaffolds/checkboxPage/app/forms/$className$FormProvider.scala create mode 100644 src/main/scaffolds/checkboxPage/app/models/$className$.scala create mode 100644 src/main/scaffolds/checkboxPage/app/pages/$className$Page.scala create mode 100644 src/main/scaffolds/checkboxPage/app/views/$className$View.scala.html create mode 100644 src/main/scaffolds/checkboxPage/default.properties create mode 100644 src/main/scaffolds/checkboxPage/generated-test/controllers/$className$ControllerSpec.scala create mode 100644 src/main/scaffolds/checkboxPage/generated-test/forms/$className$FormProviderSpec.scala create mode 100644 src/main/scaffolds/checkboxPage/generated-test/models/$className$Spec.scala create mode 100644 src/main/scaffolds/checkboxPage/generated-test/pages/$className$PageSpec.scala create mode 100644 src/main/scaffolds/checkboxPage/generated-test/views/$className$ViewSpec.scala create mode 100644 src/main/scaffolds/checkboxPage/migrations/$className__snake$.sh diff --git a/src/main/g8/app/forms/mappings/Constraints.scala b/src/main/g8/app/forms/mappings/Constraints.scala index 8c93a584..6093d3ce 100644 --- a/src/main/g8/app/forms/mappings/Constraints.scala +++ b/src/main/g8/app/forms/mappings/Constraints.scala @@ -85,4 +85,12 @@ trait Constraints { case _ => Valid } + + protected def nonEmptySet(errorKey: String): Constraint[Set[_]] = + Constraint { + case set if set.nonEmpty => + Valid + case _ => + Invalid(errorKey) + } } diff --git a/src/main/g8/app/views/components/input_checkboxes.scala.html b/src/main/g8/app/views/components/input_checkboxes.scala.html new file mode 100644 index 00000000..1f85b3ce --- /dev/null +++ b/src/main/g8/app/views/components/input_checkboxes.scala.html @@ -0,0 +1,66 @@ +@import viewmodels.RadioOption + +@( + field: Field, + legend: String, + legendClass: Option[String] = None, + hint: Option[String] = None, + trackGa: Boolean = false, + inputs: Set[RadioOption] +)(implicit messages: Messages) + +@hintBlock = { + @hint.map { hint => + + @hint + + } +} + +@errorBlock = { + @field.errors.map { error => + + @messages(error.message, error.args: _*) + + } +} + +@divClass = @{ + if(field.hasErrors) "form-group form-field--error" else "form-group" +} + +@lgndClass = @{ + legendClass match { + case Some(str) => s"bold-small \$str" + case None => "bold-small" + } +} + +
    +
    + @legend + @hintBlock + @errorBlock + @for((RadioOption(id, value, messageKey), index) <- inputs.zipWithIndex) { + @defining( + inputs.toSeq.indices.flatMap { i => + field(s"[\$i]").value + } + ) { answers => +
    + + +
    + } + } +
    +
    diff --git a/src/main/g8/test/forms/behaviours/CheckboxFieldBehaviours.scala b/src/main/g8/test/forms/behaviours/CheckboxFieldBehaviours.scala new file mode 100644 index 00000000..acc49239 --- /dev/null +++ b/src/main/g8/test/forms/behaviours/CheckboxFieldBehaviours.scala @@ -0,0 +1,45 @@ +package forms.behaviours + +import forms.FormSpec +import play.api.data.{Form, FormError} + +trait CheckboxFieldBehaviours extends FormSpec { + + def checkboxField[T](form: Form[_], + fieldName: String, + validValues: Set[T], + invalidError: FormError): Unit = { + for { + (value, i) <- validValues.zipWithIndex + } yield s"binds `\$value` successfully" in { + val data = Map( + s"\$fieldName[\$i]" -> value.toString + ) + form.bind(data).get shouldEqual Set(value) + } + + "fail to bind when the answer is invalid" in { + val data = Map( + s"\$fieldName[0]" -> "invalid value" + ) + form.bind(data).errors should contain(invalidError) + } + } + + def mandatoryCheckboxField(form: Form[_], + fieldName: String, + requiredKey: String): Unit = { + + "fail to bind when no answers are selected" in { + val data = Map.empty[String, String] + form.bind(data).errors should contain(FormError(s"\$fieldName", requiredKey)) + } + + "fail to bind when blank answer provided" in { + val data = Map( + s"\$fieldName[0]" -> "" + ) + form.bind(data).errors should contain(FormError(s"\$fieldName[0]", requiredKey)) + } + } +} diff --git a/src/main/g8/test/views/behaviours/CheckboxViewBehaviours.scala b/src/main/g8/test/views/behaviours/CheckboxViewBehaviours.scala new file mode 100644 index 00000000..127dea56 --- /dev/null +++ b/src/main/g8/test/views/behaviours/CheckboxViewBehaviours.scala @@ -0,0 +1,96 @@ +package views.behaviours + +import play.api.data.{Form, FormError} +import play.twirl.api.HtmlFormat +import viewmodels.RadioOption + +trait CheckboxViewBehaviours[A] extends ViewBehaviours { + + def checkboxPage(form: Form[Set[A]], + createView: Form[Set[A]] => HtmlFormat.Appendable, + messageKeyPrefix: String, + options: Set[RadioOption], + fieldKey: String = "value", + legend: Option[String] = None): Unit = { + + "behave like a checkbox page" must { + "contain a legend for the question" in { + val doc = asDocument(createView(form)) + val legends = doc.getElementsByTag("legend") + legends.size mustBe 1 + legends.first.text mustBe legend.getOrElse(messages(s"\$messageKeyPrefix.heading")) + } + + "contain an input for the value" in { + val doc = asDocument(createView(form)) + for { + (_, i) <- options.zipWithIndex + } yield { + assertRenderedById(doc, form(fieldKey)(s"[\$i]").id) + } + } + + "contain a label for each input" in { + val doc = asDocument(createView(form)) + for { + (option, i) <- options.zipWithIndex + } yield { + val id = form(fieldKey)(s"[\$i]").id + doc.select(s"label[for=\$id]").text mustEqual messages(option.messageKey) + } + } + + "have no values checked when rendered with no form" in { + val doc = asDocument(createView(form)) + for { + (_, i) <- options.zipWithIndex + } yield { + assert(!doc.getElementById(form(fieldKey)(s"[\$i]").id).hasAttr("checked")) + } + } + + options.zipWithIndex.foreach { + case (checkboxOption, i) => + s"have correct value checked when value `\${checkboxOption.value}` is given" in { + val data: Map[String, String] = + Map(s"\$fieldKey[\$i]" -> checkboxOption.value) + + val doc = asDocument(createView(form.bind(data))) + val field = form(fieldKey)(s"[\$i]") + + assert(doc.getElementById(field.id).hasAttr("checked"), s"\${field.id} is not checked") + + options.zipWithIndex.foreach { + case (option, j) => + if (option != checkboxOption) { + val field = form(fieldKey)(s"[\$j]") + assert(!doc.getElementById(field.id).hasAttr("checked"), s"\${field.id} is checked") + } + } + } + } + + "not render an error summary" in { + val doc = asDocument(createView(form)) + assertNotRenderedById(doc, "error-summary-heading") + } + + + "show error in the title" in { + val doc = asDocument(createView(form.withError(FormError(fieldKey, "error.invalid")))) + doc.title.contains("Error: ") mustBe true + } + + "show an error summary" in { + val doc = asDocument(createView(form.withError(FormError(fieldKey, "error.invalid")))) + assertRenderedById(doc, "error-summary-heading") + } + + "show an error in the value field's label" in { + val doc = asDocument(createView(form.withError(FormError(fieldKey, "error.invalid")))) + val errorSpan = doc.getElementsByClass("error-notification").first + errorSpan.text mustBe messages("error.invalid") + } + } + } +} diff --git a/src/main/scaffolds/checkboxPage/app/controllers/$className$Controller.scala b/src/main/scaffolds/checkboxPage/app/controllers/$className$Controller.scala new file mode 100644 index 00000000..8aa6e555 --- /dev/null +++ b/src/main/scaffolds/checkboxPage/app/controllers/$className$Controller.scala @@ -0,0 +1,56 @@ +package controllers + +import controllers.actions._ +import forms.$className$FormProvider +import javax.inject.Inject +import models.Mode +import navigation.Navigator +import pages.$className$Page +import play.api.i18n.{I18nSupport, MessagesApi} +import play.api.mvc.{Action, AnyContent, MessagesControllerComponents} +import repositories.SessionRepository +import uk.gov.hmrc.play.bootstrap.controller.FrontendBaseController +import views.html.$className$View + +import scala.concurrent.{ExecutionContext, Future} + +class $className$Controller @Inject()( + override val messagesApi: MessagesApi, + sessionRepository: SessionRepository, + navigator: Navigator, + identify: IdentifierAction, + getData: DataRetrievalAction, + requireData: DataRequiredAction, + formProvider: $className$FormProvider, + val controllerComponents: MessagesControllerComponents, + view: $className$View + )(implicit ec: ExecutionContext) extends FrontendBaseController with I18nSupport { + + val form = formProvider() + + def onPageLoad(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData) { + implicit request => + + val preparedForm = request.userAnswers.get($className$Page) match { + case None => form + case Some(value) => form.fill(value) + } + + Ok(view(preparedForm, mode)) + } + + def onSubmit(mode: Mode): Action[AnyContent] = (identify andThen getData andThen requireData).async { + implicit request => + + form.bindFromRequest().fold( + formWithErrors => + Future.successful(BadRequest(view(formWithErrors, mode))), + + value => + for { + updatedAnswers <- Future.fromTry(request.userAnswers.set($className$Page, value)) + _ <- sessionRepository.set(updatedAnswers) + } yield Redirect(navigator.nextPage($className$Page, mode, updatedAnswers)) + ) + } +} diff --git a/src/main/scaffolds/checkboxPage/app/forms/$className$FormProvider.scala b/src/main/scaffolds/checkboxPage/app/forms/$className$FormProvider.scala new file mode 100644 index 00000000..815c48cb --- /dev/null +++ b/src/main/scaffolds/checkboxPage/app/forms/$className$FormProvider.scala @@ -0,0 +1,16 @@ +package forms + +import javax.inject.Inject + +import forms.mappings.Mappings +import play.api.data.Form +import play.api.data.Forms.set +import models.$className$ + +class $className$FormProvider @Inject() extends Mappings { + + def apply(): Form[Set[$className$]] = + Form( + "value" -> set(enumerable[$className$]("$className;format="decap"$.error.required")).verifying(nonEmptySet("$className;format="decap"$.error.required")) + ) +} diff --git a/src/main/scaffolds/checkboxPage/app/models/$className$.scala b/src/main/scaffolds/checkboxPage/app/models/$className$.scala new file mode 100644 index 00000000..42c7f721 --- /dev/null +++ b/src/main/scaffolds/checkboxPage/app/models/$className$.scala @@ -0,0 +1,23 @@ +package models + +import viewmodels.RadioOption + +sealed trait $className$ + +object $className$ extends Enumerable.Implicits { + + case object $option1key;format="Camel"$ extends WithName("$option1key;format="decap"$") with $className$ + case object $option2key;format="Camel"$ extends WithName("$option2key;format="decap"$") with $className$ + + val values: Set[$className$] = Set( + $option1key;format="Camel"$, $option2key;format="Camel"$ + ) + + val options: Set[RadioOption] = values.map { + value => + RadioOption("$className;format="decap"$", value.toString) + } + + implicit val enumerable: Enumerable[$className$] = + Enumerable(values.toSeq.map(v => v.toString -> v): _*) +} diff --git a/src/main/scaffolds/checkboxPage/app/pages/$className$Page.scala b/src/main/scaffolds/checkboxPage/app/pages/$className$Page.scala new file mode 100644 index 00000000..56a2fd1e --- /dev/null +++ b/src/main/scaffolds/checkboxPage/app/pages/$className$Page.scala @@ -0,0 +1,11 @@ +package pages + +import models.$className$ +import play.api.libs.json.JsPath + +case object $className$Page extends QuestionPage[Set[$className$]] { + + override def path: JsPath = JsPath \ toString + + override def toString: String = "$className;format="decap"$" +} diff --git a/src/main/scaffolds/checkboxPage/app/views/$className$View.scala.html b/src/main/scaffolds/checkboxPage/app/views/$className$View.scala.html new file mode 100644 index 00000000..9ba5a289 --- /dev/null +++ b/src/main/scaffolds/checkboxPage/app/views/$className$View.scala.html @@ -0,0 +1,32 @@ +@import controllers.routes.$className$Controller +@import models.{Mode, $className$} + +@this( + main_template: MainTemplate, + formHelper: FormWithCSRF +) + +@(form: Form[Set[$className$]], mode: Mode)(implicit request: Request[_], messages: Messages) + +@main_template( + title = s"\${errorPrefix(form)} \${messages("$className;format="decap"$.title")}" + ) { + + @formHelper(action = $className$Controller.onSubmit(mode), 'autoComplete -> "off") { + + @components.back_link() + + @components.error_summary(form.errors) + + @components.heading("$className;format="decap"$.heading") + + @components.input_checkboxes( + field = form("value"), + legend = messages("$className;format="decap"$.heading"), + legendClass = Some("visually-hidden"), + inputs = $className$.options + ) + + @components.submit_button() + } +} diff --git a/src/main/scaffolds/checkboxPage/default.properties b/src/main/scaffolds/checkboxPage/default.properties new file mode 100644 index 00000000..b2e27987 --- /dev/null +++ b/src/main/scaffolds/checkboxPage/default.properties @@ -0,0 +1,7 @@ +description = Generates a controller and view for a page with a set of checkboxes +className = MyNewPage +title = $className;format="decap"$ +option1key = option1 +option1msg = Option 1 +option2key = option2 +option2msg = Option 2 diff --git a/src/main/scaffolds/checkboxPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/checkboxPage/generated-test/controllers/$className$ControllerSpec.scala new file mode 100644 index 00000000..dbffed6f --- /dev/null +++ b/src/main/scaffolds/checkboxPage/generated-test/controllers/$className$ControllerSpec.scala @@ -0,0 +1,150 @@ +package controllers + +import base.SpecBase +import forms.$className$FormProvider +import models.{NormalMode, $className$, UserAnswers} +import navigation.{FakeNavigator, Navigator} +import org.mockito.Matchers.any +import org.mockito.Mockito.when +import org.scalatestplus.mockito.MockitoSugar +import pages.$className$Page +import play.api.inject.bind +import play.api.libs.json.{JsString, Json} +import play.api.mvc.Call +import play.api.test.FakeRequest +import play.api.test.Helpers._ +import repositories.SessionRepository +import views.html.$className$View + +import scala.concurrent.Future + +class $className$ControllerSpec extends SpecBase with MockitoSugar { + + def onwardRoute = Call("GET", "/foo") + + lazy val $className;format="decap"$Route = routes.$className$Controller.onPageLoad(NormalMode).url + + val formProvider = new $className$FormProvider() + val form = formProvider() + + "$className$ Controller" must { + + "return OK and the correct view for a GET" in { + + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val result = route(application, request).value + + val view = application.injector.instanceOf[$className$View] + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view(form, NormalMode)(fakeRequest, messages).toString + + application.stop() + } + + "populate the view correctly on a GET when the question has previously been answered" in { + + val userAnswers = UserAnswers(userAnswersId).set($className$Page, $className$.values).success.value + + val application = applicationBuilder(userAnswers = Some(userAnswers)).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val view = application.injector.instanceOf[$className$View] + + val result = route(application, request).value + + status(result) mustEqual OK + + contentAsString(result) mustEqual + view(form.fill($className$.values), NormalMode)(fakeRequest, messages).toString + + application.stop() + } + + "redirect to the next page when valid data is submitted" in { + + val mockSessionRepository = mock[SessionRepository] + + when(mockSessionRepository.set(any())) thenReturn Future.successful(true) + + val application = + applicationBuilder(userAnswers = Some(emptyUserAnswers)) + .overrides( + bind[Navigator].toInstance(new FakeNavigator(onwardRoute)), + bind[SessionRepository].toInstance(mockSessionRepository) + ) + .build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value[0]", $className$.values.head.toString)) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual onwardRoute.url + + application.stop() + } + + "return a Bad Request and errors when invalid data is submitted" in { + + val application = applicationBuilder(userAnswers = Some(emptyUserAnswers)).build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value", "invalid value")) + + val boundForm = form.bind(Map("value" -> "invalid value")) + + val view = application.injector.instanceOf[$className$View] + + val result = route(application, request).value + + status(result) mustEqual BAD_REQUEST + + contentAsString(result) mustEqual + view(boundForm, NormalMode)(fakeRequest, messages).toString + + application.stop() + } + + "redirect to Session Expired for a GET if no existing data is found" in { + + val application = applicationBuilder(userAnswers = None).build() + + val request = FakeRequest(GET, $className;format="decap"$Route) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() + } + + "redirect to Session Expired for a POST if no existing data is found" in { + + val application = applicationBuilder(userAnswers = None).build() + + val request = + FakeRequest(POST, $className;format="decap"$Route) + .withFormUrlEncodedBody(("value[0]", $className$.values.head.toString)) + + val result = route(application, request).value + + status(result) mustEqual SEE_OTHER + + redirectLocation(result).value mustEqual routes.SessionExpiredController.onPageLoad().url + + application.stop() + } + } +} diff --git a/src/main/scaffolds/checkboxPage/generated-test/forms/$className$FormProviderSpec.scala b/src/main/scaffolds/checkboxPage/generated-test/forms/$className$FormProviderSpec.scala new file mode 100644 index 00000000..0c814ac2 --- /dev/null +++ b/src/main/scaffolds/checkboxPage/generated-test/forms/$className$FormProviderSpec.scala @@ -0,0 +1,29 @@ +package forms + +import forms.behaviours.CheckboxFieldBehaviours +import models.$className$ +import play.api.data.FormError + +class $className$FormProviderSpec extends CheckboxFieldBehaviours { + + val form = new $className$FormProvider()() + + ".value" must { + + val fieldName = "value" + val requiredKey = "$className;format="decap"$.error.required" + + behave like checkboxField[$className$]( + form, + fieldName, + validValues = $className$.values, + invalidError = FormError(s"\$fieldName[0]", "error.invalid") + ) + + behave like mandatoryCheckboxField( + form, + fieldName, + requiredKey + ) + } +} diff --git a/src/main/scaffolds/checkboxPage/generated-test/models/$className$Spec.scala b/src/main/scaffolds/checkboxPage/generated-test/models/$className$Spec.scala new file mode 100644 index 00000000..668a3eb8 --- /dev/null +++ b/src/main/scaffolds/checkboxPage/generated-test/models/$className$Spec.scala @@ -0,0 +1,47 @@ +package models + +import generators.ModelGenerators +import org.scalacheck.Arbitrary.arbitrary +import org.scalacheck.Gen +import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks +import org.scalatest.{MustMatchers, OptionValues, WordSpec} +import play.api.libs.json.{JsError, JsString, Json} + +class $className$Spec extends WordSpec with MustMatchers with ScalaCheckPropertyChecks with OptionValues with ModelGenerators { + + "$className$" must { + + "deserialise valid values" in { + + val gen = arbitrary[$className$] + + forAll(gen) { + $className;format="decap"$ => + + JsString($className;format="decap"$.toString).validate[$className$].asOpt.value mustEqual $className;format="decap"$ + } + } + + "fail to deserialise invalid values" in { + + val gen = arbitrary[String] suchThat (!$className$.values.map(_.toString).contains(_)) + + forAll(gen) { + invalidValue => + + JsString(invalidValue).validate[$className$] mustEqual JsError("error.invalid") + } + } + + "serialise" in { + + val gen = arbitrary[$className$] + + forAll(gen) { + $className;format="decap"$ => + + Json.toJson($className;format="decap"$) mustEqual JsString($className;format="decap"$.toString) + } + } + } +} diff --git a/src/main/scaffolds/checkboxPage/generated-test/pages/$className$PageSpec.scala b/src/main/scaffolds/checkboxPage/generated-test/pages/$className$PageSpec.scala new file mode 100644 index 00000000..52df73bc --- /dev/null +++ b/src/main/scaffolds/checkboxPage/generated-test/pages/$className$PageSpec.scala @@ -0,0 +1,16 @@ +package pages + +import models.$className$ +import pages.behaviours.PageBehaviours + +class $className$PageSpec extends PageBehaviours { + + "$className$Page" must { + + beRetrievable[Set[$className$]]($className$Page) + + beSettable[Set[$className$]]($className$Page) + + beRemovable[Set[$className$]]($className$Page) + } +} diff --git a/src/main/scaffolds/checkboxPage/generated-test/views/$className$ViewSpec.scala b/src/main/scaffolds/checkboxPage/generated-test/views/$className$ViewSpec.scala new file mode 100644 index 00000000..da23fd5e --- /dev/null +++ b/src/main/scaffolds/checkboxPage/generated-test/views/$className$ViewSpec.scala @@ -0,0 +1,30 @@ +package views + +import forms.$className$FormProvider +import models.{$className$, NormalMode} +import play.api.Application +import play.api.data.Form +import play.twirl.api.HtmlFormat +import views.behaviours.CheckboxViewBehaviours +import views.html.$className$View + +class $className$ViewSpec extends CheckboxViewBehaviours[$className$] { + + val messageKeyPrefix = "$className;format="decap"$" + + val form = new $className$FormProvider()() + + "$className$View" must { + + val view = viewFor[$className$View](Some(emptyUserAnswers)) + + def applyView(form: Form[Set[$className$]]): HtmlFormat.Appendable = + view.apply(form, NormalMode)(fakeRequest, messages) + + behave like normalPage(applyView(form), messageKeyPrefix) + + behave like pageWithBackLink(applyView(form)) + + behave like checkboxPage(form, applyView, messageKeyPrefix, $className$.options) + } +} diff --git a/src/main/scaffolds/checkboxPage/migrations/$className__snake$.sh b/src/main/scaffolds/checkboxPage/migrations/$className__snake$.sh new file mode 100644 index 00000000..413c1618 --- /dev/null +++ b/src/main/scaffolds/checkboxPage/migrations/$className__snake$.sh @@ -0,0 +1,75 @@ +#!/bin/bash + +echo "" +echo "Applying migration $className;format="snake"$" + +echo "Adding routes to conf/app.routes" + +echo "" >> ../conf/app.routes +echo "GET /$className;format="decap"$ controllers.$className$Controller.onPageLoad(mode: Mode = NormalMode)" >> ../conf/app.routes +echo "POST /$className;format="decap"$ controllers.$className$Controller.onSubmit(mode: Mode = NormalMode)" >> ../conf/app.routes + +echo "GET /change$className$ controllers.$className$Controller.onPageLoad(mode: Mode = CheckMode)" >> ../conf/app.routes +echo "POST /change$className$ controllers.$className$Controller.onSubmit(mode: Mode = CheckMode)" >> ../conf/app.routes + +echo "Adding messages to conf.messages" +echo "" >> ../conf/messages.en +echo "$className;format="decap"$.title = $title$" >> ../conf/messages.en +echo "$className;format="decap"$.heading = $title$" >> ../conf/messages.en +echo "$className;format="decap"$.$option1key;format="decap"$ = $option1msg$" >> ../conf/messages.en +echo "$className;format="decap"$.$option2key;format="decap"$ = $option2msg$" >> ../conf/messages.en +echo "$className;format="decap"$.checkYourAnswersLabel = $title$" >> ../conf/messages.en +echo "$className;format="decap"$.error.required = Select $className;format="decap"$" >> ../conf/messages.en + +echo "Adding to UserAnswersEntryGenerators" +awk '/trait UserAnswersEntryGenerators/ {\ + print;\ + print "";\ + print " implicit lazy val arbitrary$className$UserAnswersEntry: Arbitrary[($className$Page.type, JsValue)] =";\ + print " Arbitrary {";\ + print " for {";\ + print " page <- arbitrary[$className$Page.type]";\ + print " value <- arbitrary[$className$].map(Json.toJson(_))";\ + print " } yield (page, value)";\ + print " }";\ + next }1' ../test/generators/UserAnswersEntryGenerators.scala > tmp && mv tmp ../test/generators/UserAnswersEntryGenerators.scala + +echo "Adding to PageGenerators" +awk '/trait PageGenerators/ {\ + print;\ + print "";\ + print " implicit lazy val arbitrary$className$Page: Arbitrary[$className$Page.type] =";\ + print " Arbitrary($className$Page)";\ + next }1' ../test/generators/PageGenerators.scala > tmp && mv tmp ../test/generators/PageGenerators.scala + +echo "Adding to ModelGenerators" +awk '/trait ModelGenerators/ {\ + print;\ + print "";\ + print " implicit lazy val arbitrary$className$: Arbitrary[$className$] =";\ + print " Arbitrary {";\ + print " Gen.oneOf($className$.values.toSeq)";\ + print " }";\ + next }1' ../test/generators/ModelGenerators.scala > tmp && mv tmp ../test/generators/ModelGenerators.scala + +echo "Adding to UserAnswersGenerator" +awk '/val generators/ {\ + print;\ + print " arbitrary[($className$Page.type, JsValue)] ::";\ + next }1' ../test/generators/UserAnswersGenerator.scala > tmp && mv tmp ../test/generators/UserAnswersGenerator.scala + +echo "Adding helper method to CheckYourAnswersHelper" +awk '/class/ {\ + print;\ + print "";\ + print " def $className;format="decap"$: Option[AnswerRow] = userAnswers.get($className$Page) map {";\ + print " x =>";\ + print " AnswerRow(";\ + print " HtmlFormat.escape(messages(\"$className;format="decap"$.checkYourAnswersLabel\")),";\ + print " Html(x.map(value => HtmlFormat.escape(messages(s\"$className;format="decap"$.\$value\")).toString).mkString(\",
    \")),";\ + print " routes.$className$Controller.onPageLoad(CheckMode).url";\ + print " )" + print " }";\ + next }1' ../app/utils/CheckYourAnswersHelper.scala > tmp && mv tmp ../app/utils/CheckYourAnswersHelper.scala + +echo "Migration $className;format="snake"$ completed" From 9086e0cf2152747f16408915170dc5830be9923c Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Wed, 25 Sep 2019 08:38:14 +0100 Subject: [PATCH 22/23] Represent checkbox options as seq rather than seq --- .../g8/app/views/components/input_checkboxes.scala.html | 4 ++-- .../test/forms/behaviours/CheckboxFieldBehaviours.scala | 2 +- .../test/views/behaviours/CheckboxViewBehaviours.scala | 2 +- .../scaffolds/checkboxPage/app/models/$className$.scala | 9 +++++---- .../controllers/$className$ControllerSpec.scala | 4 ++-- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/main/g8/app/views/components/input_checkboxes.scala.html b/src/main/g8/app/views/components/input_checkboxes.scala.html index 1f85b3ce..ecf66415 100644 --- a/src/main/g8/app/views/components/input_checkboxes.scala.html +++ b/src/main/g8/app/views/components/input_checkboxes.scala.html @@ -6,7 +6,7 @@ legendClass: Option[String] = None, hint: Option[String] = None, trackGa: Boolean = false, - inputs: Set[RadioOption] + inputs: Seq[RadioOption] )(implicit messages: Messages) @hintBlock = { @@ -43,7 +43,7 @@ @errorBlock @for((RadioOption(id, value, messageKey), index) <- inputs.zipWithIndex) { @defining( - inputs.toSeq.indices.flatMap { i => + inputs.indices.flatMap { i => field(s"[\$i]").value } ) { answers => diff --git a/src/main/g8/test/forms/behaviours/CheckboxFieldBehaviours.scala b/src/main/g8/test/forms/behaviours/CheckboxFieldBehaviours.scala index acc49239..c158fde3 100644 --- a/src/main/g8/test/forms/behaviours/CheckboxFieldBehaviours.scala +++ b/src/main/g8/test/forms/behaviours/CheckboxFieldBehaviours.scala @@ -7,7 +7,7 @@ trait CheckboxFieldBehaviours extends FormSpec { def checkboxField[T](form: Form[_], fieldName: String, - validValues: Set[T], + validValues: Seq[T], invalidError: FormError): Unit = { for { (value, i) <- validValues.zipWithIndex diff --git a/src/main/g8/test/views/behaviours/CheckboxViewBehaviours.scala b/src/main/g8/test/views/behaviours/CheckboxViewBehaviours.scala index 127dea56..4b97579b 100644 --- a/src/main/g8/test/views/behaviours/CheckboxViewBehaviours.scala +++ b/src/main/g8/test/views/behaviours/CheckboxViewBehaviours.scala @@ -9,7 +9,7 @@ trait CheckboxViewBehaviours[A] extends ViewBehaviours { def checkboxPage(form: Form[Set[A]], createView: Form[Set[A]] => HtmlFormat.Appendable, messageKeyPrefix: String, - options: Set[RadioOption], + options: Seq[RadioOption], fieldKey: String = "value", legend: Option[String] = None): Unit = { diff --git a/src/main/scaffolds/checkboxPage/app/models/$className$.scala b/src/main/scaffolds/checkboxPage/app/models/$className$.scala index 42c7f721..be4c128b 100644 --- a/src/main/scaffolds/checkboxPage/app/models/$className$.scala +++ b/src/main/scaffolds/checkboxPage/app/models/$className$.scala @@ -9,15 +9,16 @@ object $className$ extends Enumerable.Implicits { case object $option1key;format="Camel"$ extends WithName("$option1key;format="decap"$") with $className$ case object $option2key;format="Camel"$ extends WithName("$option2key;format="decap"$") with $className$ - val values: Set[$className$] = Set( - $option1key;format="Camel"$, $option2key;format="Camel"$ + val values: Seq[$className$] = Seq( + $option1key;format="Camel"$, + $option2key;format="Camel"$ ) - val options: Set[RadioOption] = values.map { + val options: Seq[RadioOption] = values.map { value => RadioOption("$className;format="decap"$", value.toString) } implicit val enumerable: Enumerable[$className$] = - Enumerable(values.toSeq.map(v => v.toString -> v): _*) + Enumerable(values.map(v => v.toString -> v): _*) } diff --git a/src/main/scaffolds/checkboxPage/generated-test/controllers/$className$ControllerSpec.scala b/src/main/scaffolds/checkboxPage/generated-test/controllers/$className$ControllerSpec.scala index dbffed6f..5e389d4e 100644 --- a/src/main/scaffolds/checkboxPage/generated-test/controllers/$className$ControllerSpec.scala +++ b/src/main/scaffolds/checkboxPage/generated-test/controllers/$className$ControllerSpec.scala @@ -49,7 +49,7 @@ class $className$ControllerSpec extends SpecBase with MockitoSugar { "populate the view correctly on a GET when the question has previously been answered" in { - val userAnswers = UserAnswers(userAnswersId).set($className$Page, $className$.values).success.value + val userAnswers = UserAnswers(userAnswersId).set($className$Page, $className$.values.toSet).success.value val application = applicationBuilder(userAnswers = Some(userAnswers)).build() @@ -62,7 +62,7 @@ class $className$ControllerSpec extends SpecBase with MockitoSugar { status(result) mustEqual OK contentAsString(result) mustEqual - view(form.fill($className$.values), NormalMode)(fakeRequest, messages).toString + view(form.fill($className$.values.toSet), NormalMode)(fakeRequest, messages).toString application.stop() } From 200874b179c717e77274504efb5b8090e2fff964 Mon Sep 17 00:00:00 2001 From: Nick Wilson Date: Wed, 25 Sep 2019 08:38:47 +0100 Subject: [PATCH 23/23] Prepare v0.12.0 --- CHANGELOG.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7da5d67b..331cb39a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). +## v0.12.0 - 2019-09-24 + +### Changed +* Upgraded to Play 2.6 +* Refactored user answers to use a Json object rather than CacheMap +* Introduce paths on each page to allow pages to decide where in the Json object to store data +* Introduce queries, which can be used to look at data in UserAnswers +* Add a scaffold for a date page +* Add a scaffold for a page of checkboxes +* Updated navigator pattern +* Html escape users' answers on CYA pages +* Updated libraries and plugins +* Update assets frontend version + +## Fixed +* Stop application in unit tests +* Mock repositories in controller unit tests +* Remove unsafe-inline and data: from CSP +* Fix textarea whitespace issue +* Fix auth action recover block +* Allow field names to be specified on quesitonPage + ## v0.11.0 - 2018-09-25 ### Changed