diff --git a/build.sbt b/build.sbt index db53035..d1214de 100644 --- a/build.sbt +++ b/build.sbt @@ -14,7 +14,7 @@ * limitations under the License. */ -import uk.gov.hmrc.DefaultBuildSettings.{defaultSettings, scalaSettings} +import uk.gov.hmrc.DefaultBuildSettings.{ defaultSettings, scalaSettings } import play.twirl.sbt.Import.TwirlKeys import play.sbt.routes.RoutesKeys import scoverage.ScoverageKeys @@ -22,7 +22,7 @@ import scoverage.ScoverageKeys val appName = "secure-message-frontend" Global / majorVersion := 1 -Global / scalaVersion := "3.3.4" +Global / scalaVersion := "3.3.6" val excludedPackages: Seq[String] = Seq( "", @@ -32,13 +32,12 @@ val excludedPackages: Seq[String] = Seq( ".*\\$anon.*", "testOnlyDoNotUseInAppConf.*", "views.viewmodels.*" - ) lazy val scoverageSettings = Seq( ScoverageKeys.coverageExcludedPackages := excludedPackages.mkString(","), - ScoverageKeys.coverageMinimumStmtTotal := 67.90, + ScoverageKeys.coverageMinimumStmtTotal := 86, ScoverageKeys.coverageFailOnMinimum := true, ScoverageKeys.coverageHighlighting := true ) @@ -49,7 +48,12 @@ lazy val microservice = Project(appName, file(".")) .settings(defaultSettings() *) .settings( name := appName, - RoutesKeys.routesImport ++= Seq("models._", "controllers.generic.models._", "controllers.binders._","uk.gov.hmrc.play.bootstrap.binders.RedirectUrl"), + RoutesKeys.routesImport ++= Seq( + "models._", + "controllers.generic.models._", + "controllers.binders._", + "uk.gov.hmrc.play.bootstrap.binders.RedirectUrl" + ), libraryDependencies ++= AppDependencies.compile ++ AppDependencies.test, TwirlKeys.templateImports ++= Seq( "config.AppConfig", @@ -65,18 +69,18 @@ lazy val microservice = Project(appName, file(".")) buildInfoKeys := Seq[BuildInfoKey](name, version, scalaVersion, sbtVersion) ) .settings( + scalacOptions := scalacOptions.value.diff(Seq("-Wunused:all")), scalacOptions ++= Seq( // Silence unused imports in template files "-Wconf:msg=unused import&src=.*:s", // Silence "Flag -XXX set repeatedly" "-Wconf:msg=Flag.*repeatedly:s", // Silence unused warnings on Play `routes` files - "-Wconf:src=routes/.*:s") - + "-Wconf:src=routes/.*:s" + ) ) .settings(scoverageSettings.settings *) - lazy val it = (project in file("it")) .enablePlugins(PlayScala) .dependsOn(`microservice` % "test->test") @@ -92,6 +96,5 @@ Test / test := (Test / test) .value it / test := (it / Test / test) - .dependsOn(scalafmtCheckAll, it/scalafmtCheckAll) + .dependsOn(scalafmtCheckAll, it / scalafmtCheckAll) .value - diff --git a/it/test/ConversationPartialISpec.scala b/it/test/ConversationPartialISpec.scala index 014bd51..7bf3739 100644 --- a/it/test/ConversationPartialISpec.scala +++ b/it/test/ConversationPartialISpec.scala @@ -26,10 +26,12 @@ import views.helpers.HtmlUtil.encodeBase64String import play.api.libs.ws.JsonBodyWritables.writeableOf_JsValue class ConversationPartialISpec extends PlaySpec with ServiceSpec with MockitoSugar with BeforeAndAfterEach { - override def externalServices: Seq[String] = Seq.empty + val secureMessagePort: Int = 9051 val overCharacterLimit: Int = 4001 val id = "909d1359aa0220d12c73160a" + + override def externalServices: Seq[String] = Seq.empty override protected def beforeEach(): Unit = { wsClient .url(s"http://localhost:$secureMessagePort/test-only/delete/conversation/$id") diff --git a/it/test/MessageFrontendISpec.scala b/it/test/MessageFrontendISpec.scala index dbdd7b4..91d1de1 100644 --- a/it/test/MessageFrontendISpec.scala +++ b/it/test/MessageFrontendISpec.scala @@ -29,7 +29,7 @@ import play.api.http.{ HeaderNames, Status } import play.api.inject.guice.GuiceApplicationBuilder import play.api.libs.json.* import play.api.libs.ws.JsonBodyWritables.writeableOf_JsValue -import play.api.libs.ws.{ WSClient, WSResponse } +import play.api.libs.ws.WSClient import play.api.mvc.AnyContentAsEmpty import play.api.test.FakeRequest import play.api.test.Helpers.* @@ -39,15 +39,16 @@ import uk.gov.hmrc.crypto.{ PlainText, SymmetricCryptoFactory } import uk.gov.hmrc.domain.* import uk.gov.hmrc.http.{ HeaderCarrier, SessionKeys } -import java.time.LocalDate import java.util.Base64 import java.util.concurrent.TimeUnit import scala.concurrent.duration.{ Duration, FiniteDuration } import scala.jdk.CollectionConverters.* import scala.util.Random +import play.api.http.ContentTypes class MessageFrontendISpec extends PlaySpec with GuiceOneServerPerSuite with ScalaFutures with BeforeAndAfterEach with Eventually { + val duration15: Int = 15 implicit val defaultTimeout: FiniteDuration = Duration(duration15, TimeUnit.SECONDS) @@ -60,15 +61,16 @@ class MessageFrontendISpec ) .build() - lazy val ws = app.injector.instanceOf[WSClient] - lazy val testAuthorisationProvider = app.injector.instanceOf[TestAuthorisationProvider] - val messageResource = "http://localhost:8910/" - val secureMessageResource = "http://localhost:9051/secure-messaging/" + lazy val ws: WSClient = app.injector.instanceOf[WSClient] + lazy val testAuthorisationProvider: TestAuthorisationProvider = app.injector.instanceOf[TestAuthorisationProvider] - override protected def beforeEach() = { - ws.url(s"${messageResource}test-only/messages").delete().futureValue - ws.url(s"${messageResource}test-only/qmessages").delete().futureValue - } + val secureMessageResource = "http://localhost:9051/" + + override protected def beforeEach(): Unit = + ws.url(s"${secureMessageResource}test-only/delete/secure-messages") + .withHttpHeaders((HeaderNames.CONTENT_TYPE, ContentTypes.JSON)) + .delete() + .futureValue trait TestCase { @@ -76,8 +78,8 @@ class MessageFrontendISpec implicit val hc: HeaderCarrier = HeaderCarrier() - lazy val authBuilder = testAuthorisationProvider.governmentGatewayAuthority().withSaUtr(utr) - lazy val ggAuthorisationHeader = authBuilder.bearerTokenHeader()(Duration(1, TimeUnit.MINUTES)) + lazy val authBuilder: AuthorityBuilder = testAuthorisationProvider.governmentGatewayAuthority().withSaUtr(utr) + lazy val ggAuthorisationHeader: (String, String) = authBuilder.bearerTokenHeader()(Duration(1, TimeUnit.MINUTES)) lazy val cookie: (String, String) = authBuilder.sessionCookie(ggAuthorisationHeader._2) def authResource(path: String): String = s"http://localhost:8585/$path" @@ -87,9 +89,7 @@ class MessageFrontendISpec val duration16: Int = 16 def randomDetailsId: String = "C" + Random.alphanumeric.filter(_.isDigit).take(duration16).mkString - val now = LocalDate.now - - lazy val atsMessage = Json + lazy val atsMessage: JsObject = Json .parse(s""" | { | "externalRef":{ @@ -123,9 +123,10 @@ class MessageFrontendISpec """.stripMargin) .as[JsObject] - lazy val statementMessage = createMessageJson("mdtp", "SA300", TaxEntity("sa", utr), Some("user@email.com")) + lazy val statementMessage: JsObject = + createMessageJson("mdtp", "SA300", TaxEntity("sa", utr), Some("user@email.com")) - lazy val refundMessage = createMessageJson("mdtp", "R002A", TaxEntity("sa", utr), Some("user@email.com")) + lazy val refundMessage: JsObject = createMessageJson("mdtp", "R002A", TaxEntity("sa", utr), Some("user@email.com")) def ninoMessage(nino: Nino): JsObject = createMessageJson("mdtp", "SA300", TaxEntity("paye", nino), Some("user@email.com")) @@ -347,21 +348,15 @@ class MessageFrontendISpec def messagesPost(body: JsObject): String = { val response = httpClient - .url(s"${messageResource}messages") + .url(s"${secureMessageResource}messages") .withHttpHeaders(SessionKeys.authToken -> ggAuthorisationHeader._2) .post(body) .futureValue + response.status must be(Status.CREATED) (response.json \\ "id").map(_.as[JsString].value).head } - def externalMessagesPost(body: JsObject): WSResponse = - httpClient - .url(s"${messageResource}external/messages") - .withHttpHeaders(SessionKeys.authToken -> ggAuthorisationHeader._2) - .post(body) - .futureValue - def messages( taxIdentifiers: List[String] = List(), regimes: List[String] = List() @@ -407,7 +402,10 @@ class MessageFrontendISpec val bt = authBuilder.bearerTokenHeader() val response = - ws.url(s"${secureMessageResource}messages").withHttpHeaders(HeaderNames.AUTHORIZATION -> bt._2).get() + ws.url(s"${secureMessageResource}secure-messaging/messages") + .withHttpHeaders(HeaderNames.AUTHORIZATION -> bt._2) + .get() + val responseValue = response.futureValue responseValue.status must be(Status.OK) val alerts = Jsoup.parse(responseValue.body).getElementById("unreadMessages") @@ -454,7 +452,6 @@ class MessageFrontendISpec messagesPost(fhddsMessage(fhdds)) messagesPost(pptMessage(ppt)) messagesPost(vatMessage(vat)) - externalMessagesPost(podsMessage("HMRC-PODS-ORG.PSAID", pods.value)) messagesPost(epayeMessage(epaye)) (authProvider, nino.value, ctUtr.value, fhdds.value, vat.value, ppt.value, pods.value, epaye.value) @@ -480,8 +477,8 @@ class MessageFrontendISpec } def emailMessagesSubject(responseBody: String): List[String] = { - val parsedMessages = Jsoup.parse(responseBody) + parsedMessages .getElementsByClass("table--borderless") .first() diff --git a/it/test/MessagesISpec.scala b/it/test/MessagesISpec.scala index 360309b..3fba993 100644 --- a/it/test/MessagesISpec.scala +++ b/it/test/MessagesISpec.scala @@ -148,7 +148,6 @@ class MessagesISpec extends MessageFrontendISpec with Inspectors { status(result) must be(Status.OK) expectedMessages(contentAsString(result), 1) - } "return no messages for ct-utr user" in new TestCase { @@ -322,9 +321,6 @@ class MessagesISpec extends MessageFrontendISpec with Inspectors { .withNino(Nino("NH123456D")) .withPodsPp(HmrcPodsPpOrg("A12345678")) - externalMessagesPost(podsMessage("HMRC-PODS-ORG.PSAID", "A1234567")) - externalMessagesPost(podsMessage("HMRC-PODSPP-ORG.PSPID", "A12345678")) - val request = messagesBta(List("PSPID"), List("pods")) .withSession( authContext.bearerTokenHeader(), diff --git a/it/test/MessagesPartialsISpec.scala b/it/test/MessagesPartialsISpec.scala index 6c97576..690fc70 100644 --- a/it/test/MessagesPartialsISpec.scala +++ b/it/test/MessagesPartialsISpec.scala @@ -20,44 +20,48 @@ */ import org.scalatest.concurrent.IntegrationPatience import org.scalatest.{ BeforeAndAfterEach, Inspectors } -import play.api.Logger +import play.api.Logging import play.api.http.Status import play.api.test.FakeRequest import play.api.test.Helpers.{ route, * } import uk.gov.hmrc.auth.core.MissingBearerToken import uk.gov.hmrc.domain.SaUtr import uk.gov.hmrc.http.SessionKeys +import play.api.http.{ ContentTypes, HeaderNames } +import play.api.mvc.{ AnyContentAsEmpty, Result } +import test.utils.AuthorityBuilder +import scala.concurrent.Future import scala.concurrent.duration.* import scala.language.postfixOps class MessagesPartialsISpec - extends MessageFrontendISpec with IntegrationPatience with Inspectors with BeforeAndAfterEach { + extends MessageFrontendISpec with IntegrationPatience with Inspectors with BeforeAndAfterEach with Logging { - val logger = Logger(getClass) implicit override val patienceConfig: PatienceConfig = PatienceConfig(timeout = scaled(30 seconds), interval = scaled(200 millis)) - override protected def beforeEach() = { - ws.url(s"${messageResource}test-only/messages").delete() - ws.url(s"${messageResource}test-only/qmessages").delete() - } + override protected def beforeEach(): Unit = + ws.url(s"${secureMessageResource}test-only/delete/secure-messages") + .withHttpHeaders((HeaderNames.CONTENT_TYPE, ContentTypes.JSON)) + .delete() + .futureValue "Message link" must { "successfully view message when step and returnUrl are missing" in new TestCase { - val utr = SaUtr("1555369043") + val utr: SaUtr = SaUtr("1555369043") - val authProvider = testAuthorisationProvider.governmentGatewayAuthority().withSaUtr(utr) + val authProvider: AuthorityBuilder = testAuthorisationProvider.governmentGatewayAuthority().withSaUtr(utr) - val bt = authProvider.bearerTokenHeader() + val bt: (String, String) = authProvider.bearerTokenHeader() - val messageId = messagesPost(statementMessage) - val request = + val messageId: String = messagesPost(statementMessage) + val request: FakeRequest[AnyContentAsEmpty.type] = getMessageForEncryptedUrl(encryptSaMessageRendererReadUrl(messageId)) .withSession(SessionKeys.authToken -> bt._2) - val result = route(app, request).get + val result: Future[Result] = route(app, request).get status(result) must be(Status.OK) } diff --git a/project/AppDependencies.scala b/project/AppDependencies.scala index 636d84a..4d0f9cd 100644 --- a/project/AppDependencies.scala +++ b/project/AppDependencies.scala @@ -14,27 +14,25 @@ * limitations under the License. */ - -import sbt._ +import sbt.* object AppDependencies { - private val bootstrapVersion = "9.19.0" + private val bootstrapVersion = "10.4.0" - val compile = Seq( + val compile: Seq[ModuleID] = Seq( "uk.gov.hmrc" %% "bootstrap-frontend-play-30" % bootstrapVersion, - "uk.gov.hmrc" %% "play-frontend-hmrc-play-30" % "12.12.0", - "uk.gov.hmrc" %% "domain-play-30" % "10.0.0", + "uk.gov.hmrc" %% "play-frontend-hmrc-play-30" % "12.20.0", + "uk.gov.hmrc" %% "domain-play-30" % "13.0.0", "org.typelevel" %% "cats-core" % "2.13.0", "net.codingwell" %% "scala-guice" % "6.0.0", - "uk.gov.hmrc" %% "play-partials-play-30" % "10.1.0", + "uk.gov.hmrc" %% "play-partials-play-30" % "10.2.0", "org.jsoup" % "jsoup" % "1.21.1" ) - val test = Seq( - "uk.gov.hmrc" %% "bootstrap-test-play-30" % bootstrapVersion % Test, - "org.scalatestplus.play" %% "scalatestplus-play" % "7.0.2" % Test, - "org.scalatestplus" %% "mockito-3-4" % "3.2.10.0" % Test, - "org.mockito" % "mockito-core" % "5.18.0" % Test + val test: Seq[ModuleID] = Seq( + "uk.gov.hmrc" %% "bootstrap-test-play-30" % bootstrapVersion % Test, + "org.scalatestplus" %% "mockito-3-4" % "3.2.10.0" % Test, + "org.mockito" % "mockito-core" % "5.18.0" % Test ) } diff --git a/project/plugins.sbt b/project/plugins.sbt index f20f5a2..cac5386 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -16,10 +16,11 @@ resolvers += MavenRepository("HMRC-open-artefacts-maven2", "https://open.artefacts.tax.service.gov.uk/maven2") resolvers += Resolver.url("HMRC-open-artefacts-ivy", url("https://open.artefacts.tax.service.gov.uk/ivy2"))( - Resolver.ivyStylePatterns) + Resolver.ivyStylePatterns +) -addSbtPlugin("uk.gov.hmrc" % "sbt-auto-build" % "3.24.0") -addSbtPlugin("uk.gov.hmrc" % "sbt-distributables" % "2.6.0") -addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.8") -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.2.2") +addSbtPlugin("uk.gov.hmrc" % "sbt-auto-build" % "3.24.0") +addSbtPlugin("uk.gov.hmrc" % "sbt-distributables" % "2.6.0") +addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.9") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.2.2") diff --git a/test/config/ErrorHandlerSpec.scala b/test/config/ErrorHandlerSpec.scala index 6b59e6f..9b3e2ab 100644 --- a/test/config/ErrorHandlerSpec.scala +++ b/test/config/ErrorHandlerSpec.scala @@ -16,17 +16,28 @@ package config +import helpers.TestData.{ TEST_HEADING, TEST_MESSAGE, TEST_TITLE } import org.scalatest.concurrent.ScalaFutures import org.scalatestplus.play.PlaySpec import org.scalatestplus.play.guice.GuiceOneAppPerSuite import play.api.http.Status +import play.api.i18n.Messages +import play.api.mvc.Results.{ NotFound, Unauthorized } import play.api.test.FakeRequest -import uk.gov.hmrc.auth.core.{ BearerTokenExpired, InsufficientConfidenceLevel, InvalidBearerToken, SessionRecordNotFound } +import uk.gov.hmrc.auth.core.{ BearerTokenExpired, IncorrectCredentialStrength, InsufficientConfidenceLevel, InvalidBearerToken, MissingBearerToken, SessionRecordNotFound } +import play.api.test.Helpers.await +import play.twirl.api.Html +import views.html.ErrorTemplate +import play.api.mvc.{ AnyContentAsEmpty, RequestHeader, Result } +import play.api.test.Helpers.* +import play.api.test.CSRFTokenHelper.CSRFFRequestHeader +import play.api.test.FakeRequest +import uk.gov.hmrc.http.NotFoundException class ErrorHandlerSpec extends PlaySpec with ScalaFutures with GuiceOneAppPerSuite { trait Setup { - val errorHandler = app.injector.instanceOf[ErrorHandler] + val errorHandler: ErrorHandler = app.injector.instanceOf[ErrorHandler] } "The Error Handler" should { @@ -58,5 +69,43 @@ class ErrorHandlerSpec extends PlaySpec with ScalaFutures with GuiceOneAppPerSui result.header.status must be(Status.UNAUTHORIZED) } + + "return unauthorised in the case of MissingBearerToken" in new Setup { + val exception: MissingBearerToken = MissingBearerToken() + val result: Result = await(errorHandler.resolveError(FakeRequest(), exception)) + + result must be(Unauthorized("Unauthorised request received - Missing Bearer Token")) + } + + "return NotFound in the case of NotFoundException" in new Setup { + val exception: NotFoundException = NotFoundException("error occurred") + val result: Result = await(errorHandler.resolveError(FakeRequest(), exception)) + + result.header.status must be(NOT_FOUND) + } + + "return UNAUTHORIZED in the case of IncorrectCredentialStrength" in new Setup { + val exception: IncorrectCredentialStrength = IncorrectCredentialStrength("error occurred") + val result: Result = await(errorHandler.resolveError(FakeRequest(), exception)) + + result.header.status must be(UNAUTHORIZED) + } + + "return correct error template for provided pageTitle, heading and message" in new Setup { + implicit val fakeRequest: FakeRequest[AnyContentAsEmpty.type] = + FakeRequest("GET", "/test/path").withCSRFToken.asInstanceOf[FakeRequest[AnyContentAsEmpty.type]] + + implicit val messages: Messages = stubMessages() + implicit val appConfig: AppConfig = app.injector.instanceOf[AppConfig] + + val errorTemplate: ErrorTemplate = app.injector.instanceOf[ErrorTemplate] + + val result: Html = await(errorHandler.standardErrorTemplate(TEST_TITLE, TEST_HEADING, TEST_MESSAGE)(fakeRequest)) + + val actualTemplateBody: String = result.body + val expectedTemplateBody: String = errorTemplate(TEST_TITLE, TEST_HEADING, TEST_MESSAGE).body + + assert(actualTemplateBody.contains(TEST_MESSAGE)) + } } } diff --git a/test/config/FrontendAppConfigSpec.scala b/test/config/FrontendAppConfigSpec.scala new file mode 100644 index 0000000..9984fd8 --- /dev/null +++ b/test/config/FrontendAppConfigSpec.scala @@ -0,0 +1,80 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package config + +import org.scalatestplus.play.PlaySpec +import org.scalatestplus.play.guice.GuiceOneAppPerSuite +import play.api.{ Application, Configuration } +import com.typesafe.config.ConfigFactory +import play.api.inject.guice.GuiceApplicationBuilder + +class FrontendAppConfigSpec extends PlaySpec with GuiceOneAppPerSuite { + "btaHost" should { + "return the correct value" in new Setup { + appConfig.btaHost mustBe "http://localhost:9020" + } + } + + "btaBaseUrl" should { + "return the correct value" in new Setup { + appConfig.btaBaseUrl mustBe "http://localhost:9020/business-account" + } + } + + "ptaHost" should { + "return the correct value" in new Setup { + appConfig.ptaHost mustBe "http://localhost:9232" + } + } + + "ptaBaseUrl" should { + "return the correct value" in new Setup { + appConfig.ptaBaseUrl mustBe "http://localhost:9232/personal-account" + } + } + + "getPortalPath" should { + "return the correct value" in new Setup { + appConfig.getPortalPath("test_key") mustBe "test_key" + } + } + + trait Setup { + val config: Configuration = Configuration( + ConfigFactory.parseString( + s""" + |metrics.enabled=false, + |metrics.enabled=false + |business-account { + |host ="http://localhost:9020" + |} + |personal-account { + |host ="http://localhost:9232" + |}, + |portal { + |destinationPath { + |test_key = "test_key" + |} + |}""".stripMargin + ) + ) + + val app: Application = new GuiceApplicationBuilder().configure(config).build() + + implicit val appConfig: AppConfig = app.injector.instanceOf[FrontendAppConfig] + } +} diff --git a/test/controllers/generic/models/ConversationFiltersSpec.scala b/test/controllers/generic/models/ConversationFiltersSpec.scala new file mode 100644 index 0000000..db8520c --- /dev/null +++ b/test/controllers/generic/models/ConversationFiltersSpec.scala @@ -0,0 +1,106 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package controllers.generic.models + +import org.scalatestplus.play.PlaySpec +import play.api.libs.json.{ JsResultException, Json } + +class ConversationFiltersSpec extends PlaySpec { + + "CustomerEnrolment request model" should { + "parse a URL parameter for enrolment into its 3 part constituents" in { + CustomerEnrolment.parse("HMRC-CUS-ORG~EoriNumber~GB1234567") mustEqual CustomerEnrolment( + "HMRC-CUS-ORG", + "EoriNumber", + "GB1234567" + ) + } + } + + "CustomerEnrolment.enrolmentReads" should { + + "read and generate correct object" in { + import CustomerEnrolment.enrolmentReads + + val customerEnrolmentJson: String = + """{ + |"key":"test_key", + |"name":"HMRC_CUST", + |"value":"GB12345678" + |}""".stripMargin + + Json.parse(customerEnrolmentJson).as[CustomerEnrolment] mustBe CustomerEnrolment( + "test_key", + "HMRC_CUST", + "GB12345678" + ) + } + + "throw exception for incompatible json" in { + import CustomerEnrolment.enrolmentReads + + val customerEnrolmentJson: String = + """{ + |"name":"HMRC_CUST", + |"value":"GB12345678" + |}""".stripMargin + + intercept[JsResultException] { + Json.parse(customerEnrolmentJson).as[CustomerEnrolment] + } + } + + } + + "Tag request model" should { + "parse a URL parameter for tag into its 2 part constituents" in { + Tag.parse("notificationType~somevalue") mustEqual Tag("notificationType", "somevalue") + } + } + + "Tag.tagReads" should { + + "read and generate correct object" in { + import Tag.tagReads + + val tagJson: String = + """{ + |"key":"test_key", + |"value":"GB12345678" + |}""".stripMargin + + Json.parse(tagJson).as[Tag] mustBe Tag( + "test_key", + "GB12345678" + ) + } + + "throw exception for incompatible json" in { + import Tag.tagReads + + val tagJson: String = + """{ + |"key":"test_key", + |"value1":"GB12345678" + |}""".stripMargin + + intercept[JsResultException] { + Json.parse(tagJson).as[Tag] + } + } + } +} diff --git a/test/helpers/TestData.scala b/test/helpers/TestData.scala new file mode 100644 index 0000000..3e23db8 --- /dev/null +++ b/test/helpers/TestData.scala @@ -0,0 +1,57 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package helpers + +import java.time.LocalDate + +object TestData { + val FIVE = 5 + val TWO = 2 + + val YEAR_2025 = 2025 + + val MONTH_11 = 11 + val MONTH_10 = 10 + + val DAY_1 = 1 + val DAY_30 = 30 + + val TEST_LOCAL_DATE: LocalDate = LocalDate.of(YEAR_2025, MONTH_11, DAY_1) + + val TEST_SUBJECT = "test_subject" + val TEST_CONTENT = "test_content" + val TEST_NAME = "test_name" + val TEST_ID = "test_id" + val TEST_FORENAME = "test_forename" + val TEST_SECOND_FORENAME = "test_second_fore_name" + val TEST_SURNAME = "test_surname" + val TEST_LINE1 = "test_line1" + val TEST_LINE2 = "test_line2" + val TEST_ADDRESS_STRING = "test_address" + + val TEST_TITLE = "test_title" + val TEST_HEADING = "test_heading" + val TEST_MESSAGE = "test_message" + val TEST_CLIENT = "test_client" + val TEST_SENDER_NAME = "test_sender" + val TEST_STATUS = "test_status" + val TEST_HONOURS = "test_honours" + + val TEST_SERVICE_NAME = "test_service" + + val TEST_LANGUAGE_ENGLISH = "English" +} diff --git a/test/controllers/generic/models/MessageFiltersSpec.scala b/test/model/EPayeSpec.scala similarity index 53% rename from test/controllers/generic/models/MessageFiltersSpec.scala rename to test/model/EPayeSpec.scala index 42a06bf..09b3018 100644 --- a/test/controllers/generic/models/MessageFiltersSpec.scala +++ b/test/model/EPayeSpec.scala @@ -14,25 +14,19 @@ * limitations under the License. */ -package controllers.generic.models +package model import org.scalatestplus.play.PlaySpec -class MessageFiltersSpec extends PlaySpec { +class EPayeSpec extends PlaySpec { - "CustomerEnrolment request model" should { - "parse a URL parameter for enrolment into its 3 part constituents" in { - CustomerEnrolment.parse("HMRC-CUS-ORG~EoriNumber~GB1234567") mustEqual CustomerEnrolment( - "HMRC-CUS-ORG", - "EoriNumber", - "GB1234567" - ) - } - } + "name" should { + "return the correct value" in { + val epaye: EPaye = EPaye("test_vlaue") - "Tag request model" should { - "parse a URL parameter for tag into its 2 part constituents" in { - Tag.parse("notificationType~somevalue") mustEqual Tag("notificationType", "somevalue") + epaye.value mustBe "test_vlaue" + epaye.name mustBe "EMPREF" + epaye.toString mustBe "test_vlaue" } } } diff --git a/test/model/MessageCountSpec.scala b/test/model/MessageCountSpec.scala new file mode 100644 index 0000000..7c10d16 --- /dev/null +++ b/test/model/MessageCountSpec.scala @@ -0,0 +1,47 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +import org.scalatestplus.play.PlaySpec +import play.api.libs.json.{ JsResult, JsResultException, Json } +import helpers.TestData.FIVE + +class MessageCountSpec extends PlaySpec { + + "Json Reads" should { + "read the json correctly" in new Setup { + Json.parse(messageCountJsonString).as[MessageCount] mustBe messageCount + } + + "throw exception for invalid json" in new Setup { + intercept[JsResultException] { + Json.parse("""{}""").as[MessageCount] + } + } + } + + "Json Writes" should { + "write the object correctly" in new Setup { + Json.toJson(messageCount) mustBe Json.parse(messageCountJsonString) + } + } + + trait Setup { + val messageCount: MessageCount = MessageCount(FIVE) + val messageCountJsonString: String = """{"count":5}""".stripMargin + } +} diff --git a/test/model/MessageListItemSpec.scala b/test/model/MessageListItemSpec.scala new file mode 100644 index 0000000..b84e29f --- /dev/null +++ b/test/model/MessageListItemSpec.scala @@ -0,0 +1,140 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +import org.scalatestplus.play.PlaySpec +import play.api.libs.json.{ JsResultException, Json } + +import java.time.format.DateTimeFormatter +import java.time.{ Instant, LocalDate, LocalDateTime } +import java.util.Locale +import helpers.TestData.{ TEST_ADDRESS_STRING, TEST_FORENAME, TEST_HONOURS, TEST_ID, TEST_LINE1, TEST_LINE2, TEST_LOCAL_DATE, TEST_SECOND_FORENAME, TEST_SUBJECT, TEST_SURNAME, TEST_TITLE } + +class MessageListItemSpec extends PlaySpec { + + "messageListItemFormat" should { + + import MessageListItem.messageListItemFormat + + "read the json correctly" in new Setup { + Json + .parse(messageListItemJsonStringWithCustomReadTimeInstant) + .as[MessageListItem] mustBe messageListItem.copy(readTime = None) + } + + "throw exception while reading the invalid json" in new Setup { + intercept[JsResultException] { + Json.parse(messageListItemJsonStringInvalid).as[MessageListItem] + } + } + + "write the object correctly" in new Setup { + Json.toJson(messageListItem) mustBe Json.parse(messageListItemJsonStringWithDefaultReadTimeInstant) + } + } + + "lang" should { + + "return the correct language value" in new Setup { + messageListItem.copy(language = Some("cy")).lang mustBe "cy" + messageListItem.lang mustBe "en" + } + } + + trait Setup { + val readTime: Instant = Instant.parse("2025-11-01T23:30:00Z") + val readTimeInstantForCustomReads: Instant = Instant.parse("+643699-01-11T15:50:00Z") + + val taxpayerName: TaxpayerName = TaxpayerName( + title = Some(TEST_TITLE), + forename = Some(TEST_FORENAME), + secondForename = Some(TEST_SECOND_FORENAME), + surname = Some(TEST_SURNAME), + honours = Some(TEST_HONOURS), + line1 = Some(TEST_LINE1), + line2 = Some(TEST_LINE2) + ) + + val messageListItem: MessageListItem = MessageListItem( + id = TEST_ID, + subject = TEST_SUBJECT, + validFrom = TEST_LOCAL_DATE, + taxpayerName = Some(taxpayerName), + readTime = Some(readTime), + sentInError = false, + replyTo = Some(TEST_ADDRESS_STRING), + messageDesc = None, + counter = Some(1), + language = None + ) + + val messageListItemJsonStringWithDefaultReadTimeInstant: String = + s"""{ + |"id":"test_id", + |"subject":"test_subject", + |"validFrom":"2025-11-01", + |"taxpayerName":{"title":"test_title", + |"forename":"test_forename", + |"secondForename":"test_second_fore_name", + |"surname":"test_surname", + |"honours":"test_honours", + |"line1":"test_line1", + |"line2":"test_line2"}, + |"readTime":"2025-11-01T23:30:00Z", + |"sentInError":false, + |"replyTo":"test_address", + |"counter":1 + |}""".stripMargin + + val messageListItemJsonStringWithCustomReadTimeInstant: String = + s"""{ + |"id":"test_id", + |"subject":"test_subject", + |"validFrom":"2025-11-01", + |"taxpayerName":{ + |"title":"test_title", + |"forename":"test_forename", + |"secondForename":"test_second_fore_name", + |"surname":"test_surname", + |"honours":"test_honours", + |"line1":"test_line1", + |"line2":"test_line2" + |}, + |"sentInError":false, + |"replyTo":"test_address", + |"counter":1 + |}""".stripMargin + + val messageListItemJsonStringInvalid: String = + s"""{ + |"id":"test_id", + |"validFrom":"2025-11-01", + |"taxpayerName":{ + |"title":"test_title", + |"forename":"test_forename", + |"secondForename":"test_second_fore_name", + |"surname":"test_surname", + |"honours":"test_honours", + |"line1":"test_line1", + |"line2":"test_line2" + |}, + |"sentInError":false, + |"replyTo":"test_address", + |"counter":1 + |}""".stripMargin + } +} diff --git a/test/model/MessagesCountsSpec.scala b/test/model/MessagesCountsSpec.scala index fe2cfa3..251b10e 100644 --- a/test/model/MessagesCountsSpec.scala +++ b/test/model/MessagesCountsSpec.scala @@ -18,9 +18,14 @@ package model import com.codahale.metrics.SharedMetricRegistries import org.scalatestplus.play.PlaySpec +import play.api.libs.json.{ JsResultException, Json } + +import java.time.{ Instant, LocalDate } +import helpers.TestData.{ FIVE, TEST_ADDRESS_STRING, TEST_FORENAME, TEST_HONOURS, TEST_ID, TEST_LINE1, TEST_LINE2, TEST_LOCAL_DATE, TEST_SECOND_FORENAME, TEST_SUBJECT, TEST_SURNAME, TEST_TITLE, TWO } class MessagesCountsSpec extends PlaySpec { - "transformReadPreference" must { + + "MessagesCounts.transformReadPreference" must { val counts = MessagesCounts(total = 3, unread = 1) "return a function to extract the total count when no read preference is provided" in { @@ -29,13 +34,153 @@ class MessagesCountsSpec extends PlaySpec { none mustBe both none mustBe MessageCount(3) } + "return a function to extract the count of unread messages when a `No` preference is provided" in { MessagesCounts.transformReadPreference(Some(ReadPreference.No))(counts) mustBe MessageCount(1) } + "return a function to extract the count of read messages when a `Yes` preference is provided" in { MessagesCounts.transformReadPreference(Some(ReadPreference.Yes))(counts) mustBe MessageCount(2) } - SharedMetricRegistries.clear + SharedMetricRegistries.clear() + } + + "MessagesCounts.format" should { + + "read the json correctly" in new Setup { + Json.parse(messagesCountsJsonString).as[MessagesCounts] mustBe messagesCounts + } + + "throw exception for invalid json" in new Setup { + intercept[JsResultException] { + Json.parse(messagesCountsJsonStringInvalid).as[MessagesCounts] + } + } + + "write the object correctly" in new Setup { + Json.toJson(messagesCounts) mustBe Json.parse(messagesCountsJsonString) + } + } + + "MessagesWithCount.format" should { + import MessagesWithCount.format + + "read the json correctly" in new Setup { + val updatedItems: Seq[MessageListItem] = Seq(messageListItem.copy(readTime = None)) + + Json + .parse(messagesWithCountCustomReadTimeInstantJsonString) + .as[MessagesWithCount] mustBe messagesWithCount.copy(items = updatedItems) + } + + "throw exception for invalid json" in new Setup { + intercept[JsResultException] { + Json.parse(messagesWithCountJsonStringInvalid).as[MessagesWithCount] + } + } + + "write the object correctly" in new Setup { + Json.toJson(messagesWithCount) mustBe Json.parse(messagesWithCountJsonString) + } + } + + trait Setup { + val readTime: Instant = Instant.parse("2025-11-01T23:30:00Z") + + val taxpayerName: TaxpayerName = TaxpayerName( + title = Some(TEST_TITLE), + forename = Some(TEST_FORENAME), + secondForename = Some(TEST_SECOND_FORENAME), + surname = Some(TEST_SURNAME), + honours = Some(TEST_HONOURS), + line1 = Some(TEST_LINE1), + line2 = Some(TEST_LINE2) + ) + + val messageListItem: MessageListItem = MessageListItem( + id = TEST_ID, + subject = TEST_SUBJECT, + validFrom = TEST_LOCAL_DATE, + taxpayerName = Some(taxpayerName), + readTime = Some(readTime), + sentInError = false, + replyTo = Some(TEST_ADDRESS_STRING), + messageDesc = None, + counter = Some(1), + language = None + ) + + val messagesCounts: MessagesCounts = MessagesCounts(total = FIVE, unread = TWO) + + val messagesWithCount: MessagesWithCount = + MessagesWithCount(items = Seq(messageListItem), count = messagesCounts) + + val messagesCountsJsonString: String = """{"total":5,"unread":2}""".stripMargin + val messagesCountsJsonStringInvalid: String = """{"total":5}""".stripMargin + + val messagesWithCountJsonString: String = + """{ + |"items":[ + |{"id":"test_id", + |"subject":"test_subject", + |"validFrom":"2025-11-01", + |"taxpayerName": + |{"title":"test_title", + |"forename":"test_forename", + |"secondForename":"test_second_fore_name", + |"surname":"test_surname", + |"honours":"test_honours", + |"line1":"test_line1", + |"line2":"test_line2" + |}, + |"readTime":"2025-11-01T23:30:00Z", + |"sentInError":false, + |"replyTo":"test_address", + |"counter":1 + |}], + |"count":{"total":5,"unread":2}}""".stripMargin + + val messagesWithCountCustomReadTimeInstantJsonString: String = + """{ + |"items":[ + |{"id":"test_id", + |"subject":"test_subject", + |"validFrom":"2025-11-01", + |"taxpayerName": + |{"title":"test_title", + |"forename":"test_forename", + |"secondForename":"test_second_fore_name", + |"surname":"test_surname", + |"honours":"test_honours", + |"line1":"test_line1", + |"line2":"test_line2" + |}, + |"sentInError":false, + |"replyTo":"test_address", + |"counter":1 + |}], + |"count":{"total":5,"unread":2}}""".stripMargin + + val messagesWithCountJsonStringInvalid: String = + """{ + |"items":[ + |{"id":"test_id", + |"validFrom":"2025-11-01", + |"taxpayerName": + |{"title":"test_title", + |"forename":"test_forename", + |"secondForename":"test_second_fore_name", + |"surname":"test_surname", + |"honours":"test_honours", + |"line1":"test_line1", + |"line2":"test_line2" + |}, + |"readTime":"2025-11-01T23:30:00Z", + |"sentInError":false, + |"replyTo":"test_address", + |"counter":1 + |}], + |"count":{"total":5,"unread":2}}""".stripMargin } } diff --git a/test/model/ReadPreferenceSpec.scala b/test/model/ReadPreferenceSpec.scala new file mode 100644 index 0000000..d93c80a --- /dev/null +++ b/test/model/ReadPreferenceSpec.scala @@ -0,0 +1,41 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package model + +import org.scalatestplus.play.PlaySpec +import ReadPreference.{ Both, No, Yes } + +class ReadPreferenceSpec extends PlaySpec { + + "validate" should { + "return the correct Enum value for the valid input" in { + val result: Either[String, ReadPreference.Value] = ReadPreference.validate("Yes") + result mustBe Right(Yes) + + val result1: Either[String, ReadPreference.Value] = ReadPreference.validate("No") + result1 mustBe Right(No) + + val result2: Either[String, ReadPreference.Value] = ReadPreference.validate("Both") + result2 mustBe Right(Both) + } + + "return the error text for invalid values" in { + val result: Either[String, ReadPreference.Value] = ReadPreference.validate("Unknown") + result.swap mustBe Right("unknown read preference: Unknown") + } + } +} diff --git a/test/models/ConversationSpec.scala b/test/models/ConversationSpec.scala new file mode 100644 index 0000000..b53df88 --- /dev/null +++ b/test/models/ConversationSpec.scala @@ -0,0 +1,198 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package models + +import com.ibm.icu.text.SimpleDateFormat +import org.scalatestplus.play.PlaySpec + +import java.time.{ Instant, LocalDate, LocalDateTime } +import play.api.libs.json.{ JsResultException, JsString, Json } +import models.{ Conversation, SenderInformation } +import com.fasterxml.jackson.core.JsonParseException +import helpers.TestData.{ TEST_CLIENT, TEST_CONTENT, TEST_ID, TEST_LANGUAGE_ENGLISH, TEST_NAME, TEST_STATUS, TEST_SUBJECT } + +import java.time.format.DateTimeFormatter +import java.util.Date + +class ConversationSpec extends PlaySpec { + + "Conversation.conversationFormat" should { + import Conversation.conversationFormat + + "read the json correctly" in new Setup { + Json.parse(conversationJsonString).as[Conversation] mustBe conversationObject + } + + "throw exception for invalid json" in new Setup { + intercept[JsResultException] { + Json.parse(conversationJsonInvalid).as[Conversation] + } + } + } + + "Message.messageReads" should { + import Message.messageReads + + "read the json correctly" in new Setup { + Json.parse(messageJson).as[Message] mustBe message + } + + "throw exception for invalid json" in new Setup { + intercept[JsResultException] { + Json.parse(messageJsonInvalid).as[Message] + } + } + } + + "SenderInformation.dateFormat" should { + import SenderInformation.dateFormat + + "read the json correctly" in new Setup { + Json.parse(timeInstantString1).as[Instant] mustBe timeInstant1 + } + + "throw exception for invalid json" in new Setup { + intercept[JsonParseException] { + Json.parse(timeInstantString).as[Instant] + } + } + + "generate correct output for Json Writes" in new Setup { + Json.toJson(timeInstant) mustBe JsString(timeInstantString) + } + } + + "SenderInformation.senderInformationFormat" should { + import SenderInformation.senderInformationFormat + + "read the json correctly" in new Setup { + Json.parse(senderInformationJson).as[SenderInformation] mustBe senderInformation + } + + "throw exception for invalid json" in new Setup { + intercept[JsResultException] { + Json.parse(senderInformationJsonInvalid).as[SenderInformation] + } + } + } + + "FirstReaderInformation.dateFormat" should { + import FirstReaderInformation.dateFormat + + "read the json correctly" in new Setup { + Json.parse(timeInstantString1).as[Instant] mustBe timeInstant1 + } + + "throw exception for invalid json" in new Setup { + intercept[JsonParseException] { + Json.parse(timeInstantString).as[Instant] + } + } + + "generate correct output for Json Writes" in new Setup { + Json.toJson(timeInstant) mustBe JsString(timeInstantString) + } + } + + "FirstReaderInformation.firstReaderFormat" should { + import FirstReaderInformation.firstReaderFormat + + "read the json correctly" in new Setup { + Json.parse(firstReaderInformationJson).as[FirstReaderInformation] mustBe firstReaderInformation + } + + "throw exception for invalid json" in new Setup { + intercept[JsResultException] { + Json.parse(firstReaderInformationJsonInvalid).as[FirstReaderInformation] + } + } + } + + trait Setup { + val timeInstant: Instant = Instant.parse("2025-10-12T23:30:00Z") + val timeInstant1: Instant = Instant.parse("+643699-01-11T15:50:00Z") + val timeInstantString: String = "2025-10-12T23:30:00Z" + val timeInstantString1: String = "20251012233000000" + + val senderInformationJson: String = + """{ + |"name":"test_name", + |"sent":20251012233000000, + |"self":true + |}""".stripMargin + + val senderInformationJsonInvalid: String = + """{ + |"name":"test_name", + |"self":false + |}""".stripMargin + + val senderInformation: SenderInformation = + SenderInformation(name = Some(TEST_NAME), sent = timeInstant1, self = true) + + val firstReaderInformationJson: String = + """{ + |"name":"test_name", + |"read":20251012233000000 + |}""".stripMargin + + val firstReaderInformationJsonInvalid: String = + """{ + |"name":"test_name" + |}""".stripMargin + + val firstReaderInformation: FirstReaderInformation = + FirstReaderInformation(name = Some(TEST_NAME), read = timeInstant1) + + val messageJson: String = """{ + |"senderInformation":{"name":"test_name","sent":20251012233000000,"self":true}, + |"content":"test_content"}""".stripMargin + + val messageJsonInvalid: String = + """{ + |"content":"test_content"}""".stripMargin + + val message: Message = Message(senderInformation = senderInformation, firstReader = None, content = TEST_CONTENT) + + val conversationJsonString: String = + s"""{ + |"client":"test_client", + |"conversationId":"test_id", + |"status":"test_status", + |"subject":"test_subject", + |"language":"English", + |"messages":[$messageJson] + |}""".stripMargin + + val conversationJsonInvalid: String = s"""{ + |"client":"test_client", + |"conversationId":"test_id", + |"status":"test_status", + |"messages":[$messageJson] + |}""".stripMargin + + val conversationObject: Conversation = Conversation( + client = TEST_CLIENT, + conversationId = TEST_ID, + status = TEST_STATUS, + tags = None, + subject = TEST_SUBJECT, + language = TEST_LANGUAGE_ENGLISH, + messages = List(message) + ) + } +} diff --git a/test/models/CountSpec.scala b/test/models/CountSpec.scala index 25ed7c4..530bbd5 100644 --- a/test/models/CountSpec.scala +++ b/test/models/CountSpec.scala @@ -17,18 +17,36 @@ package models import org.scalatestplus.play.PlaySpec -import play.api.libs.json.Json +import play.api.libs.json.{ JsResultException, Json } +import helpers.TestData.{ FIVE, TWO } class CountSpec extends PlaySpec { - "A count result as JSON" must { - "have its content serialised as expected" in { - val totalMessagesCount: Long = 5 - val unreadMessagesCount: Long = 2 + "Json Reads" should { + import Count.countFormat - val countResult = Count(total = totalMessagesCount, unread = unreadMessagesCount) - Json.toJson(countResult) mustEqual Json.parse("""{"total":5,"unread":2}""") + "read the json correctly" in new Setup { + Json.parse(countJsonString).as[Count] mustBe count } + + "throw exception for invalid json" in new Setup { + intercept[JsResultException] { + Json.parse(countJsonStringInvalid).as[Count] + } + } + } + + "Json Writes" should { + "write the object correctly" in new Setup { + Json.toJson(count) mustBe Json.parse(countJsonString) + } + } + + trait Setup { + val count: Count = Count(total = FIVE, unread = TWO) + + val countJsonString: String = """{"total":5,"unread":2}""".stripMargin + val countJsonStringInvalid = """{"total":5}""" } } diff --git a/test/models/LetterSpec.scala b/test/models/LetterSpec.scala new file mode 100644 index 0000000..a943b30 --- /dev/null +++ b/test/models/LetterSpec.scala @@ -0,0 +1,178 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package models + +import com.fasterxml.jackson.core.JsonParseException +import helpers.TestData.{ DAY_30, MONTH_10, TEST_CONTENT, TEST_NAME, TEST_SUBJECT, YEAR_2025 } +import org.scalatestplus.play.PlaySpec +import play.api.libs.json.{ JsResultException, JsString, Json } +import views.helpers.DateFormat + +import java.time.format.DateTimeFormatter +import java.time.{ Instant, LocalDate, LocalDateTime } + +class LetterSpec extends PlaySpec { + + "messageFormat" should { + import Letter.messageFormat + + "read the json correctly" in new Setup { + Json.parse(letterJsonStringWithCustomReads).as[Letter] mustBe letterObjectForCustomJsonReads + } + + "throw exception for invalid json" in new Setup { + intercept[JsResultException] { + Json.parse(letterJsonStringInvalid).as[Letter] + } + } + + "generate correct output for Json Writes" in new Setup { + Json.toJson(letter) mustBe Json.parse(letterJsonString) + } + } + + "senderFormat" should { + import Letter.senderFormat + + "read the json correctly" in new Setup { + Json.parse(senderJsonString).as[Sender] mustBe sender + } + + "throw exception for invalid json" in new Setup { + intercept[JsResultException] { + Json.parse(senderJsonStringInvalid).as[Sender] + } + } + + "generate correct output for Json Writes" in new Setup { + Json.toJson(sender) mustBe Json.parse(senderJsonString) + } + } + + "dateTimeFormat" should { + import Letter.dateTimeFormat + + "read the json correctly" in new Setup { + Json.parse(timeInstantJsonString).as[Instant] mustBe timeInstant + } + + "throw exception for invalid json" in new Setup { + intercept[JsonParseException] { + Json.parse(timeInstantJsonStringInvalid).as[Instant] + } + } + + "generate correct output for Json Writes" in new Setup { + Json.toJson(defaultTimeInstant) mustBe JsString(defaultTimeInstantJsonString) + } + } + + "dateFormat" should { + import Letter.dateFormat + + "read the json correctly" in new Setup { + Json.parse("20250101").as[LocalDate] mustBe date1 + } + + "throw exception for invalid json" in new Setup { + intercept[JsonParseException] { + Json.parse("2025:10:30").as[LocalDate] + } + } + + "generate correct output for Json Writes" in new Setup { + Json.toJson(date) mustBe JsString(dateJsonString) + } + } + + trait Setup { + val timeInstantJsonString: String = "20251012233000000" + val dateJsonString = "2025-10-30" + + val timeInstantJsonStringInvalid: String = "2025-10-12T23:30:00Z" + val defaultTimeInstantJsonString: String = "2025-10-12T23:30:00Z" + + val defaultTimeInstant: Instant = Instant.parse("2025-10-12T23:30:00Z") + val timeInstant: Instant = Instant.parse("+643699-01-11T15:50:00Z") + + val year1970 = 1970 + + val date: LocalDate = LocalDate.of(YEAR_2025, MONTH_10, DAY_30) + val date1: LocalDate = LocalDate.of(year1970, 1, 1) + + val senderJsonString: String = + """{ + |"name":"test_name", + |"sent":"2025-10-30" + |}""".stripMargin + + val senderJsonStringInvalid: String = """{ + |"name":"test_name" + |}""".stripMargin + + val sender: Sender = Sender(name = TEST_NAME, sent = date) + + val firstReaderInformationJson: String = + """{ + |"name":"test_name", + |"read":20251012233000000 + |}""".stripMargin + + val firstReaderInformation: FirstReaderInformation = + FirstReaderInformation(name = Some(TEST_NAME), read = timeInstant) + + val letterJsonString: String = + s"""{ + |"subject":"test_subject", + |"content":"test_content", + |"senderInformation":$senderJsonString, + |"readTime":"2025-10-12T23:30:00Z" + |}""".stripMargin + + val letterJsonStringWithCustomReads: String = + s"""{ + |"subject":"test_subject", + |"content":"test_content", + |"firstReaderInformation":$firstReaderInformationJson, + |"senderInformation":$senderJsonString, + |"readTime":20251012233000000 + |}""".stripMargin + + val letterJsonStringInvalid: String = + s"""{ + |"subject":"test_subject", + |"content":"test_content", + |"readTime":"2025-10-12T23:30:00Z" + |}""".stripMargin + + val letter: Letter = Letter( + subject = TEST_SUBJECT, + content = TEST_CONTENT, + firstReaderInformation = None, + senderInformation = sender, + readTime = Some(defaultTimeInstant) + ) + + val letterObjectForCustomJsonReads: Letter = Letter( + subject = TEST_SUBJECT, + content = TEST_CONTENT, + firstReaderInformation = Some(firstReaderInformation), + senderInformation = sender, + readTime = Some(timeInstant) + ) + } +} diff --git a/test/models/MessageHeaderSpec.scala b/test/models/MessageHeaderSpec.scala new file mode 100644 index 0000000..1f2d574 --- /dev/null +++ b/test/models/MessageHeaderSpec.scala @@ -0,0 +1,115 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package models + +import helpers.TestData.{ FIVE, TEST_CLIENT, TEST_ID, TEST_SENDER_NAME, TEST_SUBJECT } +import org.scalatestplus.play.PlaySpec + +import java.time.Instant +import play.api.libs.json.{ JsResultException, JsString, Json } + +class MessageHeaderSpec extends PlaySpec { + + "Json Reads" should { + import MessageHeader.conversationHeaderReads + + "read the json correctly" in new Setup { + Json.parse(messageHeaderJsonStringForCustomReads).as[MessageHeader] mustBe messageHeaderWithCustomIssueDate + } + + "throw exception for the invalid json" in new Setup { + intercept[JsResultException] { + Json.parse(messageHeaderJsonInvalidString).as[MessageHeader] + } + } + } + + "Json Writes" should { + "write the object correctly" in new Setup { + Json.toJson(messageHeader) mustBe Json.parse(messageHeaderJsonString) + } + } + + trait Setup { + val issueDate: Instant = Instant.parse("2025-10-12T23:30:00Z") + val issueDateFosCustomReads: Instant = Instant.parse("+643699-01-11T15:50:00Z") + + val issueDateJsonString = "2025-10-12T23:30:00Z" + val issueDateJsonForCustomRead = "20251012233000000" + + val messageHeader: MessageHeader = MessageHeader( + messageType = MessageType.Conversation, + id = TEST_ID, + subject = TEST_SUBJECT, + issueDate = issueDate, + senderName = Some(TEST_SENDER_NAME), + unreadMessages = true, + count = FIVE, + conversationId = Some(TEST_ID), + client = Some(TEST_CLIENT) + ) + + val messageHeaderWithCustomIssueDate: MessageHeader = MessageHeader( + messageType = MessageType.Conversation, + id = TEST_ID, + subject = TEST_SUBJECT, + issueDate = issueDateFosCustomReads, + senderName = Some(TEST_SENDER_NAME), + unreadMessages = true, + count = FIVE, + conversationId = Some(TEST_ID), + client = Some(TEST_CLIENT) + ) + + val messageHeaderJsonString: String = + """{ + |"messageType":"conversation", + |"id":"test_id", + |"subject":"test_subject", + |"issueDate":"2025-10-12T23:30:00Z", + |"senderName":"test_sender", + |"unreadMessages":true, + |"count":5, + |"conversationId":"test_id", + |"client":"test_client" + |}""".stripMargin + + val messageHeaderJsonStringForCustomReads: String = + """{ + |"messageType":"conversation", + |"id":"test_id", + |"subject":"test_subject", + |"issueDate":20251012233000000, + |"senderName":"test_sender", + |"unreadMessages":true, + |"count":5, + |"conversationId":"test_id", + |"client":"test_client" + |}""".stripMargin + + val messageHeaderJsonInvalidString: String = + """{ + |"messageType":"conversation", + |"id":"test_id", + |"subject":"test_subject", + |"senderName":"test_sender", + |"unreadMessages":true, + |"conversationId":"test_id", + |"client":"test_client" + |}""".stripMargin + } +} diff --git a/test/views/helpers/NameCaseSpec.scala b/test/views/helpers/NameCaseSpec.scala new file mode 100644 index 0000000..ba680d4 --- /dev/null +++ b/test/views/helpers/NameCaseSpec.scala @@ -0,0 +1,59 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package views.helpers + +import org.scalatestplus.play.PlaySpec + +class NameCaseSpec extends PlaySpec { + + "nc" should { + + "return the correct name with correct case" in { + NameCase.nc("Mack") mustBe "Mack" + NameCase.nc("Macky") mustBe "Macky" + NameCase.nc("Mace") mustBe "Mace" + NameCase.nc("\bMacEvicius") mustBe "\bMacevicius" + NameCase.nc("\bMacHado") mustBe "\bMachado" + NameCase.nc("\bMacHar") mustBe "\bMachar" + NameCase.nc("\bMacHin") mustBe "\bMachin" + NameCase.nc("\bMacHlin") mustBe "\bMachlin" + NameCase.nc("\bMacIas") mustBe "\bMacias" + NameCase.nc("\bMacIulis") mustBe "\bMaciulis" + NameCase.nc("\bMacKie") mustBe "\bMackie" + NameCase.nc("\bMacKle") mustBe "\bMackle" + NameCase.nc("\bMacKlin") mustBe "\bMacklin" + NameCase.nc("\bMacQuarie") mustBe "\bMacquarie" + NameCase.nc("\bMacOmber") mustBe "\bMacomber" + NameCase.nc("\bMacIn") mustBe "\bMacin" + NameCase.nc("\bMacKintosh") mustBe "\bMackintosh" + NameCase.nc("\bMacKen") mustBe "\bMacken" + NameCase.nc("\bMacHen") mustBe "\bMachen" + NameCase.nc("\bMacisaac") mustBe "\bMacisaac" + NameCase.nc("\bMacHiel") mustBe "\bMachiel" + NameCase.nc("\bMacIol") mustBe "\bMaciol" + NameCase.nc("\bMacKell") mustBe "\bMackell" + NameCase.nc("\bMacKlem") mustBe "\bMacklem" + NameCase.nc("\bMacKrell") mustBe "\bMackrell" + NameCase.nc("\bMacLin") mustBe "\bMaclin" + NameCase.nc("\bMacKey") mustBe "\bMackey" + NameCase.nc("\bMacKley") mustBe "\bMackley" + NameCase.nc("\bMacHell") mustBe "\bMachell" + NameCase.nc("\bMacHon") mustBe "\bMachon" + NameCase.nc("\bMacmurdo") mustBe "\bMacMurdo" + } + } +} diff --git a/test/views/partials/MessageInboxSpec.scala b/test/views/partials/MessageInboxSpec.scala new file mode 100644 index 0000000..d0ecf86 --- /dev/null +++ b/test/views/partials/MessageInboxSpec.scala @@ -0,0 +1,79 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package views.partials + +import helpers.TestData.{ FIVE, TEST_CLIENT, TEST_HEADING, TEST_ID, TEST_NAME, TEST_SERVICE_NAME, TEST_SUBJECT, TWO } +import models.{ Conversation, MessageHeader, MessageType } +import org.scalatestplus.play.PlaySpec +import org.scalatestplus.play.guice.GuiceOneAppPerSuite +import play.api.i18n.Messages +import play.api.test.Helpers.stubMessages +import play.twirl.api.HtmlFormat +import views.html.partials.messageInbox +import views.viewmodels.MessageInbox + +import java.time.Instant + +class MessageInboxSpec extends PlaySpec with GuiceOneAppPerSuite { + + "view" should { + "display the correct contents" in { + + val msgHeader = MessageHeader( + messageType = MessageType.Conversation, + id = TEST_ID, + subject = TEST_SUBJECT, + issueDate = Instant.now(), + senderName = Some(TEST_NAME), + unreadMessages = true, + count = FIVE, + conversationId = Some(TEST_ID), + client = Some(TEST_CLIENT) + ) + + val msgInboxModel = MessageInbox( + clientService = TEST_SERVICE_NAME, + heading = TEST_HEADING, + total = FIVE, + unread = TWO, + conversationHeaders = List(msgHeader) + ) + + implicit val messages: Messages = stubMessages() + + val view: HtmlFormat.Appendable = app.injector.instanceOf[messageInbox].apply(msgInboxModel) + + val viewBody: String = view.body + + assert(viewBody.contains("

test_heading

")) + assert(viewBody.contains("conversation.inbox.heading.message")) + assert(viewBody.contains("conversation.inbox.heading.date")) + assert(viewBody.contains("conversation.inbox.heading.from")) + assert(viewBody.contains("conversation.inbox.heading.subject")) + assert(viewBody.contains("conversation.inbox.subject.count")) + + assert( + viewBody.contains( + "" + + "2 conversation.inbox.heading.unread, 5 conversation.inbox.heading.total. conversation.inbox.heading.description" + + "" + ) + ) + } + } + +} diff --git a/test/views/partials/MessageReplySpec.scala b/test/views/partials/MessageReplySpec.scala new file mode 100644 index 0000000..2e403b1 --- /dev/null +++ b/test/views/partials/MessageReplySpec.scala @@ -0,0 +1,83 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package views.partials + +import org.scalatestplus.play.PlaySpec +import org.scalatestplus.play.guice.GuiceOneAppPerSuite +import play.api.data.FormError +import play.api.i18n.Messages +import play.api.mvc.AnyContentAsEmpty +import play.api.test.FakeRequest +import play.twirl.api.{ Html, HtmlFormat } +import views.html.partials.messageReply +import views.viewmodels.MessageReply +import play.api.test.CSRFTokenHelper.CSRFFRequestHeader +import play.api.test.Helpers.stubMessages + +class MessageReplySpec extends PlaySpec with GuiceOneAppPerSuite { + + "view" should { + "display the correct contents" when { + + "there is no form error" in { + implicit val messages: Messages = stubMessages() + implicit val fakeRequest: FakeRequest[AnyContentAsEmpty.type] = + FakeRequest("GET", "/test/path").withCSRFToken.asInstanceOf[FakeRequest[AnyContentAsEmpty.type]] + + val msgReplyModel = MessageReply( + showReplyForm = true, + replyFormUrl = "test_url", + replyIcon = Html("test_content"), + formErrors = Seq(), + content = "test_content" + ) + + val view: HtmlFormat.Appendable = app.injector.instanceOf[messageReply].apply(msgReplyModel) + val viewBody: String = view.body + + assert(viewBody.contains("test_url")) + assert(viewBody.contains("test_content")) + assert(viewBody.contains("conversation.reply.form.heading")) + assert(viewBody.contains("conversation.reply.form.send.button")) + } + + "there is a form error" in { + implicit val messages: Messages = stubMessages() + implicit val fakeRequest: FakeRequest[AnyContentAsEmpty.type] = + FakeRequest("GET", "/test/path").withCSRFToken.asInstanceOf[FakeRequest[AnyContentAsEmpty.type]] + + val msgReplyModel = MessageReply( + showReplyForm = true, + replyFormUrl = "test_url", + replyIcon = Html("test_content"), + formErrors = Seq(FormError("value", List("conversation.reply.form.exceeded.length"), Seq.empty)), + content = "test_content" + ) + + val view: HtmlFormat.Appendable = app.injector.instanceOf[messageReply].apply(msgReplyModel) + val viewBody: String = view.body + + assert(viewBody.contains("test_url")) + assert(viewBody.contains("test_content")) + assert(viewBody.contains("conversation.reply.form.heading")) + assert(viewBody.contains("conversation.reply.form.send.button")) + assert(viewBody.contains("Error:")) + assert(viewBody.contains("conversation.reply.form.exceeded.length")) + } + } + } +} diff --git a/test/views/partials/MessageResultSpec.scala b/test/views/partials/MessageResultSpec.scala new file mode 100644 index 0000000..7312d07 --- /dev/null +++ b/test/views/partials/MessageResultSpec.scala @@ -0,0 +1,43 @@ +/* + * Copyright 2023 HM Revenue & Customs + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package views.partials + +import org.scalatestplus.play.PlaySpec +import org.scalatestplus.play.guice.GuiceOneAppPerSuite +import play.api.i18n.Messages +import play.api.test.Helpers.stubMessages +import play.twirl.api.HtmlFormat +import views.html.partials.messageResult + +class MessageResultSpec extends PlaySpec with GuiceOneAppPerSuite { + + "view" should { + "display the correct contents" in { + implicit val messages: Messages = stubMessages() + val messageInboxUrl = "test_inbox_url" + + val view: HtmlFormat.Appendable = app.injector.instanceOf[messageResult].apply(messageInboxUrl) + + val viewBody: String = view.body + + assert(viewBody.contains("confirmation.what.next")) + assert(viewBody.contains("confirmation.step1")) + assert(viewBody.contains("confirmation.step2")) + assert(viewBody.contains("confirmation.back")) + } + } +}