From 3f5c23060392327b626344db452b085bb9fb8afd Mon Sep 17 00:00:00 2001 From: "Saqib@Ee" Date: Tue, 30 Jul 2024 15:08:25 +0100 Subject: [PATCH 1/3] CIR-1651: Some cleaning up of the controller --- .gitignore | 29 ++++++------ app/controllers/AddressSearchController.scala | 46 ++++++++----------- app/model/request.scala | 9 ++++ 3 files changed, 41 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index 482562b..ac58b88 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,22 @@ - -logs -project/project +**/.bloop/ +**/.metals/ +**/.vscode/ **/target/ -lib_managed -tmp +*.iws .history -dist -/.idea /*.iml /*.ipr -/out -/.idea_modules +/.bsp /.classpath +/.idea +/.idea_modules /.project -/RUNNING_PID /.settings -*.iws -/.bsp /null -**/.bloop/ -**/.metals/ -**/.vscode/ +/out +/RUNNING_PID +dist +lib_managed +logs +project/project +tmp diff --git a/app/controllers/AddressSearchController.scala b/app/controllers/AddressSearchController.scala index dc0e8fc..3f78c2d 100644 --- a/app/controllers/AddressSearchController.scala +++ b/app/controllers/AddressSearchController.scala @@ -64,7 +64,6 @@ class AddressSearchController @Inject()(connector: DownstreamConnector, auditCon def searchByCountry(countryCode: String): Action[LookupByCountryFilterRequest] = accessCheckedAction(parse.json[LookupByCountryFilterRequest]) { implicit request: Request[LookupByCountryFilterRequest] => - implicit val hc: HeaderCarrier = HeaderCarrierConverter.fromRequest(request) val newRequest: Request[LookupByCountryRequest] = request.withTarget(RequestTarget("/country/lookup", "/country/lookup", request.queryString)) .withBody(addCountryTo(request.body, countryCode.toLowerCase)) @@ -73,51 +72,37 @@ class AddressSearchController @Inject()(connector: DownstreamConnector, auditCon searchByCountry(newRequest) } - private[controllers] def searchByUprn(request: Request[LookupByUprnRequest]): Future[Result] = { - implicit val hc: HeaderCarrier = HeaderCarrierConverter.fromRequest(request) - val userAgent = request.headers.get(HeaderNames.USER_AGENT) - - val uprn: LookupByUprnRequest = request.body - + private[controllers] def searchByUprn(request: Request[LookupByUprnRequest])(implicit hc: HeaderCarrier, userAgent: Option[UserAgent]): Future[Result] = { import model.address.AddressRecord.formats._ forwardIfAllowed[LookupByUprnRequest, List[AddressRecord]](request, - addresses => auditAddressSearch(userAgent, addresses, uprn = Some(uprn.uprn)) + addresses => auditAddressSearch(userAgent, addresses, uprn = Some(request.body.uprn)) ) } - private[controllers] def searchByPostcode[A](request: Request[LookupByPostcodeRequest]): Future[Result] = { - implicit val hc: HeaderCarrier = HeaderCarrierConverter.fromRequest(request) - val userAgent = request.headers.get(HeaderNames.USER_AGENT) + private[controllers] def searchByPostcode[A](request: Request[LookupByPostcodeRequest])(implicit hc: HeaderCarrier, userAgent: Option[UserAgent]): Future[Result] = { + import model.address.AddressRecord.formats._ val postcode: LookupByPostcodeRequest = request.body - import model.address.AddressRecord.formats._ - forwardIfAllowed[LookupByPostcodeRequest, List[AddressRecord]](request, addresses => auditAddressSearch(userAgent, addresses, postcode = Some(postcode.postcode), filter = postcode.filter)) } - private[controllers] def searchByTown[A](request: Request[LookupByPostTownRequest]): Future[Result] = { - implicit val hc: HeaderCarrier = HeaderCarrierConverter.fromRequest(request) - val userAgent = request.headers.get(HeaderNames.USER_AGENT) + private[controllers] def searchByTown[A](request: Request[LookupByPostTownRequest])(implicit hc: HeaderCarrier, userAgent: Option[UserAgent]): Future[Result] = { + import model.address.AddressRecord.formats._ val posttown: LookupByPostTownRequest = request.body - import model.address.AddressRecord.formats._ - forwardIfAllowed[LookupByPostTownRequest, List[AddressRecord]](request, addresses => auditAddressSearch(userAgent, addresses, posttown = Some(posttown.posttown.toUpperCase), filter = posttown.filter)) } - private[controllers] def searchByCountry[A](request: Request[LookupByCountryRequest])(implicit hc: HeaderCarrier): Future[Result] = { - implicit val hc: HeaderCarrier = HeaderCarrierConverter.fromRequest(request) - val userAgent = request.headers.get(HeaderNames.USER_AGENT) + private[controllers] def searchByCountry[A](request: Request[LookupByCountryRequest])(implicit hc: HeaderCarrier, userAgent: Option[UserAgent]): Future[Result] = { + import model.address.NonUKAddress._ val country: LookupByCountryRequest = request.body - import model.address.NonUKAddress._ - forwardIfAllowed[LookupByCountryRequest, List[NonUKAddress]](request, addresses => auditNonUKAddressSearch(userAgent, country = country.country, filter = Option(country.filter), nonUKAddresses = addresses)) } @@ -143,15 +128,20 @@ class AddressSearchController @Inject()(connector: DownstreamConnector, auditCon case (BAD_REQUEST, err) => BadRequest(err) case (FORBIDDEN, err) => Forbidden(err) } - } + implicit def requestToHeaderCarrier[T](implicit request: Request[T]): HeaderCarrier = + HeaderCarrierConverter.fromRequest(request) + + implicit def requestToUserAgent[T](implicit request: Request[T]): Option[UserAgent] = + UserAgent(request) + connector.checkConnectivity(url("/ping/ping"), configHelper.addressSearchApiAuthToken).map { case true => logger.warn("Downstream connectivity to address-search-api service successfully established") case _ => logger.error("Downstream connectivity check to address-search-api service FAILED") } - private def auditAddressSearch[A](userAgent: Option[String], addressRecords: List[AddressRecord], postcode: Option[Postcode] = None, + private def auditAddressSearch[A](userAgent: Option[UserAgent], addressRecords: List[AddressRecord], postcode: Option[Postcode] = None, posttown: Option[String] = None, uprn: Option[String] = None, filter: Option[String] = None)(implicit hc: HeaderCarrier): Unit = { if (addressRecords.nonEmpty) { @@ -174,19 +164,19 @@ class AddressSearchController @Inject()(connector: DownstreamConnector, auditCon } auditConnector.sendExplicitAudit("AddressSearch", - AddressSearchAuditEvent(userAgent, + AddressSearchAuditEvent(userAgent.map(_.unwrap), auditEventRequestDetails, addressRecords.length, addressSearchAuditEventMatchedAddresses)) } } - private def auditNonUKAddressSearch[A](userAgent: Option[String], nonUKAddresses: List[NonUKAddress], country: String, + private def auditNonUKAddressSearch[A](userAgent: Option[UserAgent], nonUKAddresses: List[NonUKAddress], country: String, filter: Option[String] = None)(implicit hc: HeaderCarrier): Unit = { if (nonUKAddresses.nonEmpty) { auditConnector.sendExplicitAudit("NonUKAddressSearch", - NonUKAddressSearchAuditEvent(userAgent, + NonUKAddressSearchAuditEvent(userAgent.map(_.unwrap), NonUKAddressSearchAuditEventRequestDetails(filter), nonUKAddresses.length, nonUKAddresses.map { ma => diff --git a/app/model/request.scala b/app/model/request.scala index c94457a..a39fe46 100644 --- a/app/model/request.scala +++ b/app/model/request.scala @@ -17,9 +17,11 @@ package model import model.address.Postcode +import play.api.http.HeaderNames import play.api.libs.functional.syntax._ import play.api.libs.json.Reads._ import play.api.libs.json._ +import play.api.mvc.Request object request { @@ -87,4 +89,11 @@ object request { implicit val reads: Reads[LookupByCountryRequest] = Json.reads[LookupByCountryRequest] implicit val writes: Writes[LookupByCountryRequest] = Json.writes[LookupByCountryRequest] } + + final case class UserAgent(unwrap: String) + + object UserAgent { + def apply(request: Request[_]): Option[UserAgent] = + request.headers.get(HeaderNames.USER_AGENT).map(ua => UserAgent(ua)) + } } From b68a4d3f2b341364967d005f74d046ccfcf5c0d4 Mon Sep 17 00:00:00 2001 From: "Saqib@Ee" Date: Tue, 30 Jul 2024 15:38:08 +0100 Subject: [PATCH 2/3] CIR-1651: Move auditing code out of the controller. --- app/audit/Auditor.scala | 81 +++++++++++++++++++ app/controllers/AddressSearchController.scala | 68 ++-------------- .../AddressSearchControllerTest.scala | 13 ++- 3 files changed, 94 insertions(+), 68 deletions(-) create mode 100644 app/audit/Auditor.scala diff --git a/app/audit/Auditor.scala b/app/audit/Auditor.scala new file mode 100644 index 0000000..cb87cbb --- /dev/null +++ b/app/audit/Auditor.scala @@ -0,0 +1,81 @@ +/* + * Copyright 2024 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 audit + +import model.{AddressSearchAuditEvent, AddressSearchAuditEventMatchedAddress, AddressSearchAuditEventRequestDetails, NonUKAddressSearchAuditEvent, NonUKAddressSearchAuditEventMatchedAddress, NonUKAddressSearchAuditEventRequestDetails} +import model.address.{AddressRecord, NonUKAddress, Postcode} +import model.request.UserAgent +import uk.gov.hmrc.http.HeaderCarrier +import uk.gov.hmrc.play.audit.http.connector.AuditConnector + +import javax.inject.Inject +import scala.concurrent.ExecutionContext + +class Auditor @Inject()(auditConnector: AuditConnector)(implicit ec: ExecutionContext) { + def auditAddressSearch[A](userAgent: Option[UserAgent], addressRecords: List[AddressRecord], postcode: Option[Postcode] = None, + posttown: Option[String] = None, uprn: Option[String] = None, filter: Option[String] = None)(implicit hc: HeaderCarrier): Unit = { + + if (addressRecords.nonEmpty) { + val auditEventRequestDetails = AddressSearchAuditEventRequestDetails(postcode.map(_.toString), posttown, uprn, filter) + val addressSearchAuditEventMatchedAddresses = addressRecords.map { ma => + AddressSearchAuditEventMatchedAddress( + ma.uprn.map(_.toString).getOrElse(""), + ma.parentUprn, + ma.usrn, + ma.organisation, + ma.address.lines, + ma.address.town, + ma.localCustodian, + ma.location, + ma.administrativeArea, + ma.poBox, + ma.address.postcode, + ma.address.subdivision, + ma.address.country) + } + + auditConnector.sendExplicitAudit("AddressSearch", + AddressSearchAuditEvent(userAgent.map(_.unwrap), + auditEventRequestDetails, + addressRecords.length, + addressSearchAuditEventMatchedAddresses)) + } + } + + def auditNonUKAddressSearch[A](userAgent: Option[UserAgent], nonUKAddresses: List[NonUKAddress], country: String, + filter: Option[String] = None)(implicit hc: HeaderCarrier): Unit = { + + if (nonUKAddresses.nonEmpty) { + auditConnector.sendExplicitAudit("NonUKAddressSearch", + NonUKAddressSearchAuditEvent(userAgent.map(_.unwrap), + NonUKAddressSearchAuditEventRequestDetails(filter), + nonUKAddresses.length, + nonUKAddresses.map { ma => + NonUKAddressSearchAuditEventMatchedAddress( + ma.id, + ma.number, + ma.street, + ma.unit, + ma.city, + ma.district, + ma.region, + ma.postcode, + country) + })) + } + } +} diff --git a/app/controllers/AddressSearchController.scala b/app/controllers/AddressSearchController.scala index 3f78c2d..5da4b9a 100644 --- a/app/controllers/AddressSearchController.scala +++ b/app/controllers/AddressSearchController.scala @@ -17,10 +17,10 @@ package controllers import access.AccessChecker +import audit.Auditor import config.AppConfig import connectors.DownstreamConnector -import model._ -import model.address.{AddressRecord, NonUKAddress, Postcode} +import model.address.{AddressRecord, NonUKAddress} import model.request._ import org.apache.pekko.actor.ActorSystem import org.apache.pekko.stream.Materializer @@ -30,14 +30,13 @@ import play.api.libs.json._ import play.api.mvc._ import play.api.mvc.request.RequestTarget import uk.gov.hmrc.http.HeaderCarrier -import uk.gov.hmrc.play.audit.http.connector.AuditConnector import uk.gov.hmrc.play.bootstrap.backend.controller.BackendController import uk.gov.hmrc.play.http.HeaderCarrierConverter import javax.inject.Inject import scala.concurrent.{ExecutionContext, Future} -class AddressSearchController @Inject()(connector: DownstreamConnector, auditConnector: AuditConnector, cc: ControllerComponents, val configHelper: AppConfig)(implicit ec: ExecutionContext) +class AddressSearchController @Inject()(connector: DownstreamConnector, auditor: Auditor, cc: ControllerComponents, val configHelper: AppConfig)(implicit ec: ExecutionContext) extends BackendController(cc) with Logging with AccessChecker { private val actorSystem = ActorSystem("AddressSearchController") private implicit val materializer: Materializer = Materializer.createMaterializer(actorSystem) @@ -76,7 +75,7 @@ class AddressSearchController @Inject()(connector: DownstreamConnector, auditCon import model.address.AddressRecord.formats._ forwardIfAllowed[LookupByUprnRequest, List[AddressRecord]](request, - addresses => auditAddressSearch(userAgent, addresses, uprn = Some(request.body.uprn)) + addresses => auditor.auditAddressSearch(userAgent, addresses, uprn = Some(request.body.uprn)) ) } @@ -86,7 +85,7 @@ class AddressSearchController @Inject()(connector: DownstreamConnector, auditCon val postcode: LookupByPostcodeRequest = request.body forwardIfAllowed[LookupByPostcodeRequest, List[AddressRecord]](request, - addresses => auditAddressSearch(userAgent, addresses, postcode = Some(postcode.postcode), filter = postcode.filter)) + addresses => auditor.auditAddressSearch(userAgent, addresses, postcode = Some(postcode.postcode), filter = postcode.filter)) } private[controllers] def searchByTown[A](request: Request[LookupByPostTownRequest])(implicit hc: HeaderCarrier, userAgent: Option[UserAgent]): Future[Result] = { @@ -95,7 +94,7 @@ class AddressSearchController @Inject()(connector: DownstreamConnector, auditCon val posttown: LookupByPostTownRequest = request.body forwardIfAllowed[LookupByPostTownRequest, List[AddressRecord]](request, - addresses => auditAddressSearch(userAgent, addresses, posttown = Some(posttown.posttown.toUpperCase), filter = posttown.filter)) + addresses => auditor.auditAddressSearch(userAgent, addresses, posttown = Some(posttown.posttown.toUpperCase), filter = posttown.filter)) } private[controllers] def searchByCountry[A](request: Request[LookupByCountryRequest])(implicit hc: HeaderCarrier, userAgent: Option[UserAgent]): Future[Result] = { @@ -104,7 +103,7 @@ class AddressSearchController @Inject()(connector: DownstreamConnector, auditCon val country: LookupByCountryRequest = request.body forwardIfAllowed[LookupByCountryRequest, List[NonUKAddress]](request, - addresses => auditNonUKAddressSearch(userAgent, country = country.country, filter = Option(country.filter), nonUKAddresses = addresses)) + addresses => auditor.auditNonUKAddressSearch(userAgent, country = country.country, filter = Option(country.filter), nonUKAddresses = addresses)) } private def addCountryTo(body: LookupByCountryFilterRequest, country: String): LookupByCountryRequest = { @@ -140,57 +139,4 @@ class AddressSearchController @Inject()(connector: DownstreamConnector, auditCon case true => logger.warn("Downstream connectivity to address-search-api service successfully established") case _ => logger.error("Downstream connectivity check to address-search-api service FAILED") } - - private def auditAddressSearch[A](userAgent: Option[UserAgent], addressRecords: List[AddressRecord], postcode: Option[Postcode] = None, - posttown: Option[String] = None, uprn: Option[String] = None, filter: Option[String] = None)(implicit hc: HeaderCarrier): Unit = { - - if (addressRecords.nonEmpty) { - val auditEventRequestDetails = AddressSearchAuditEventRequestDetails(postcode.map(_.toString), posttown, uprn, filter) - val addressSearchAuditEventMatchedAddresses = addressRecords.map { ma => - AddressSearchAuditEventMatchedAddress( - ma.uprn.map(_.toString).getOrElse(""), - ma.parentUprn, - ma.usrn, - ma.organisation, - ma.address.lines, - ma.address.town, - ma.localCustodian, - ma.location, - ma.administrativeArea, - ma.poBox, - ma.address.postcode, - ma.address.subdivision, - ma.address.country) - } - - auditConnector.sendExplicitAudit("AddressSearch", - AddressSearchAuditEvent(userAgent.map(_.unwrap), - auditEventRequestDetails, - addressRecords.length, - addressSearchAuditEventMatchedAddresses)) - } - } - - private def auditNonUKAddressSearch[A](userAgent: Option[UserAgent], nonUKAddresses: List[NonUKAddress], country: String, - filter: Option[String] = None)(implicit hc: HeaderCarrier): Unit = { - - if (nonUKAddresses.nonEmpty) { - auditConnector.sendExplicitAudit("NonUKAddressSearch", - NonUKAddressSearchAuditEvent(userAgent.map(_.unwrap), - NonUKAddressSearchAuditEventRequestDetails(filter), - nonUKAddresses.length, - nonUKAddresses.map { ma => - NonUKAddressSearchAuditEventMatchedAddress( - ma.id, - ma.number, - ma.street, - ma.unit, - ma.city, - ma.district, - ma.region, - ma.postcode, - country) - })) - } - } } diff --git a/test/controllers/AddressSearchControllerTest.scala b/test/controllers/AddressSearchControllerTest.scala index 5a6fa70..dbd1597 100644 --- a/test/controllers/AddressSearchControllerTest.scala +++ b/test/controllers/AddressSearchControllerTest.scala @@ -16,6 +16,7 @@ package controllers +import audit.Auditor import config.AppConfig import connectors.DownstreamConnector import model.address._ @@ -30,16 +31,16 @@ import org.scalatestplus.mockito.MockitoSugar import org.scalatestplus.play.guice.GuiceOneAppPerSuite import play.api.http.{HeaderNames, MimeTypes, Status} import play.api.inject.guice.GuiceApplicationBuilder -import play.api.libs.json.{JsNumber, JsObject, JsString, Json} -import play.api.mvc.{ControllerComponents, Request} +import play.api.libs.json.{JsNumber, JsObject, JsString} +import play.api.mvc.ControllerComponents import play.api.test.FakeRequest import play.api.test.Helpers._ import play.api.{Application, inject} import uk.gov.hmrc.play.audit.http.connector.AuditConnector import util.Utils._ +import scala.concurrent.ExecutionContext import scala.concurrent.duration._ -import scala.concurrent.{ExecutionContext, Future} import scala.language.postfixOps class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOneAppPerSuite with MockitoSugar { @@ -151,7 +152,6 @@ class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOn """when search is called without valid 'user-agent' header it should give a forbidden response and not log any error """ in { - import LookupByPostcodeRequest._ val payload = LookupByPostcodeRequest(Postcode("FX11 4HG")) val request = FakeRequest("POST", "/lookup") @@ -254,7 +254,6 @@ class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOn "uprn lookup with POST request" should { "give forbidden" when { """search is called without a valid user agent""" in { - import LookupByUprnRequest._ val payload = LookupByUprnRequest("0123456789") val request = FakeRequest("POST", "/lookup/by-uprn") .withBody(payload) @@ -268,7 +267,6 @@ class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOn "give success" when { """search is called with a valid uprn""" in { - import LookupByUprnRequest._ clearInvocations(mockAuditConnector) @@ -298,7 +296,8 @@ class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOn """search is called with an invalid uprn""" in { val connector = app.injector.instanceOf[DownstreamConnector] val configHelper = app.injector.instanceOf[AppConfig] - val controller = new AddressSearchController(connector, mockAuditConnector, cc, configHelper)(ec) + val auditor = app.injector.instanceOf[Auditor] + val controller = new AddressSearchController(connector, auditor, cc, configHelper)(ec) val payload = LookupByUprnRequest("GB0123456789") val request = FakeRequest("POST", "/lookup/by-uprn") .withBody(payload) From 2855e36bd7f6b4907f0f45cb54f85e806868cbbb Mon Sep 17 00:00:00 2001 From: "Saqib@Ee" Date: Tue, 30 Jul 2024 15:49:19 +0100 Subject: [PATCH 3/3] CIR-1651: reformat files. --- .scalafmt.conf | 2 + app/Module.scala | 3 +- app/access/AccessChecker.scala | 67 ++- app/apiplatform/DocumentationController.scala | 14 +- app/audit/Auditor.scala | 64 ++- app/config/AppConfig.scala | 20 +- app/config/InvalidJsonErrorHandler.scala | 86 +-- app/connectors/DownstreamConnector.scala | 97 ++-- app/controllers/AddressSearchController.scala | 194 +++++-- app/model/AddressSearchAuditEvent.scala | 118 +++-- app/model/NonUKAddressSearchAuditEvent.scala | 33 +- app/model/address/Address.scala | 60 ++- app/model/address/AddressRecord.scala | 93 ++-- app/model/address/Country.scala | 30 +- app/model/address/Location.scala | 3 +- app/model/address/NonUKAddress.scala | 11 +- app/model/address/Outcode.scala | 1 - app/model/address/Postcode.scala | 22 +- app/model/request.scala | 60 ++- app/model/response.scala | 15 +- app/util/package.scala | 2 +- project/plugins.sbt | 12 +- .../AddressSearchControllerTest.scala | 493 +++++++++++++++--- test/model/AddressTest.scala | 109 ++-- test/model/PostcodeTest.scala | 12 +- test/model/RequestTest.scala | 24 +- test/util/utils.scala | 4 +- 27 files changed, 1186 insertions(+), 463 deletions(-) create mode 100644 .scalafmt.conf diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..8df0f87 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,2 @@ +version = 3.7.15 +runner.dialect = scala213 diff --git a/app/Module.scala b/app/Module.scala index 5bb7e0a..cb8d54e 100644 --- a/app/Module.scala +++ b/app/Module.scala @@ -17,7 +17,8 @@ import com.google.inject.AbstractModule import play.api.{Configuration, Environment} -class Module(environment: Environment, configuration: Configuration) extends AbstractModule { +class Module(environment: Environment, configuration: Configuration) + extends AbstractModule { override def configure(): Unit = {} } diff --git a/app/access/AccessChecker.scala b/app/access/AccessChecker.scala index c1f8271..a6c0ff3 100644 --- a/app/access/AccessChecker.scala +++ b/app/access/AccessChecker.scala @@ -16,7 +16,11 @@ package access -import access.AccessChecker.{accessControlAllowListAbsoluteKey, accessControlEnabledAbsoluteKey, accessRequestFormUrlAbsoluteKey} +import access.AccessChecker.{ + accessControlAllowListAbsoluteKey, + accessControlEnabledAbsoluteKey, + accessRequestFormUrlAbsoluteKey +} import config.AppConfig import org.slf4j.LoggerFactory import play.api.http.HeaderNames @@ -34,11 +38,21 @@ trait AccessChecker { private val logger = LoggerFactory.getLogger(this.getClass) - private val accessRequestFormUrl: String = configHelper.mustGetConfigString(accessRequestFormUrlAbsoluteKey) + private val accessRequestFormUrl: String = + configHelper.mustGetConfigString(accessRequestFormUrlAbsoluteKey) - private val checkAllowList: Boolean = configHelper.mustGetConfigString(accessControlEnabledAbsoluteKey).toBoolean - private val allowedClients: Set[String] = configHelper.config.getOptional[Seq[String]](accessControlAllowListAbsoluteKey).getOrElse( - if (checkAllowList) throw new RuntimeException(s"Could not find config $accessControlAllowListAbsoluteKey") else Seq()).toSet + private val checkAllowList: Boolean = + configHelper.mustGetConfigString(accessControlEnabledAbsoluteKey).toBoolean + private val allowedClients: Set[String] = configHelper.config + .getOptional[Seq[String]](accessControlAllowListAbsoluteKey) + .getOrElse( + if (checkAllowList) + throw new RuntimeException( + s"Could not find config $accessControlAllowListAbsoluteKey" + ) + else Seq() + ) + .toSet private def areClientsAllowed(clients: Seq[String]): Boolean = clients.forall(allowedClients.contains) @@ -46,7 +60,9 @@ trait AccessChecker { private def forbiddenResponse(clients: Seq[String]): String = s"""{ |"code": ${FORBIDDEN}, - |"description": "One or more user agents in '${clients.mkString(",")}' are not authorized to use this service. Please complete '${accessRequestFormUrl}' to request access." + |"description": "One or more user agents in '${clients.mkString( + "," + )}' are not authorized to use this service. Please complete '${accessRequestFormUrl}' to request access." |}""".stripMargin private def getClientsFromRequest[T](req: Request[T]): Seq[String] = { @@ -59,30 +75,37 @@ trait AccessChecker { } } - def accessCheckedAction[A](bodyParser: BodyParser[A])(block: Request[A] => Future[Result]): Action[A] = { - Action.async(bodyParser) { - request => - val callingClients = getClientsFromRequest(request) - if (!areClientsAllowed(callingClients)) { - if (checkAllowList) { - Future.successful(Forbidden(Json.parse(forbiddenResponse(callingClients)))) - } else { - logger.warn(s"One or more user agents in '${callingClients.mkString(",")}' are not authorized to use this service") - block(request) - } - } - else { + def accessCheckedAction[A]( + bodyParser: BodyParser[A] + )(block: Request[A] => Future[Result]): Action[A] = { + Action.async(bodyParser) { request => + val callingClients = getClientsFromRequest(request) + if (!areClientsAllowed(callingClients)) { + if (checkAllowList) { + Future.successful( + Forbidden(Json.parse(forbiddenResponse(callingClients))) + ) + } else { + logger.warn( + s"One or more user agents in '${callingClients.mkString(",")}' are not authorized to use this service" + ) block(request) } + } else { + block(request) + } } } } object AccessChecker { val accessRequestFormUrlKey = "access-control.request.formUrl" - val accessRequestFormUrlAbsoluteKey = s"microservice.services.$accessRequestFormUrlKey" + val accessRequestFormUrlAbsoluteKey = + s"microservice.services.$accessRequestFormUrlKey" val accessControlEnabledKey = "access-control.enabled" - val accessControlEnabledAbsoluteKey = s"microservice.services.$accessControlEnabledKey" + val accessControlEnabledAbsoluteKey = + s"microservice.services.$accessControlEnabledKey" val accessControlAllowListKey = "access-control.allow-list" - val accessControlAllowListAbsoluteKey = s"microservice.services.$accessControlAllowListKey" + val accessControlAllowListAbsoluteKey = + s"microservice.services.$accessControlAllowListKey" } diff --git a/app/apiplatform/DocumentationController.scala b/app/apiplatform/DocumentationController.scala index 65ab933..69c814b 100644 --- a/app/apiplatform/DocumentationController.scala +++ b/app/apiplatform/DocumentationController.scala @@ -25,11 +25,19 @@ import uk.gov.hmrc.play.bootstrap.backend.controller.BackendController import javax.inject.Inject import scala.concurrent.Future -class DocumentationController @Inject()(assets: Assets, cc: ControllerComponents, configHelper: AppConfig) extends BackendController(cc) { - private val apiStatus = configHelper.mustGetConfigString("api-platform.status") +class DocumentationController @Inject() ( + assets: Assets, + cc: ControllerComponents, + configHelper: AppConfig +) extends BackendController(cc) { + private val apiStatus = + configHelper.mustGetConfigString("api-platform.status") def definition(): Action[AnyContent] = Action.async { - Future.successful(Ok(txt.definition(apiStatus)).as(ContentTypes.withCharset(MimeTypes.JSON)(Codec.utf_8))) + Future.successful( + Ok(txt.definition(apiStatus)) + .as(ContentTypes.withCharset(MimeTypes.JSON)(Codec.utf_8)) + ) } def raml(version: String, file: String): Action[AnyContent] = { diff --git a/app/audit/Auditor.scala b/app/audit/Auditor.scala index cb87cbb..c3bcc63 100644 --- a/app/audit/Auditor.scala +++ b/app/audit/Auditor.scala @@ -16,7 +16,14 @@ package audit -import model.{AddressSearchAuditEvent, AddressSearchAuditEventMatchedAddress, AddressSearchAuditEventRequestDetails, NonUKAddressSearchAuditEvent, NonUKAddressSearchAuditEventMatchedAddress, NonUKAddressSearchAuditEventRequestDetails} +import model.{ + AddressSearchAuditEvent, + AddressSearchAuditEventMatchedAddress, + AddressSearchAuditEventRequestDetails, + NonUKAddressSearchAuditEvent, + NonUKAddressSearchAuditEventMatchedAddress, + NonUKAddressSearchAuditEventRequestDetails +} import model.address.{AddressRecord, NonUKAddress, Postcode} import model.request.UserAgent import uk.gov.hmrc.http.HeaderCarrier @@ -25,12 +32,25 @@ import uk.gov.hmrc.play.audit.http.connector.AuditConnector import javax.inject.Inject import scala.concurrent.ExecutionContext -class Auditor @Inject()(auditConnector: AuditConnector)(implicit ec: ExecutionContext) { - def auditAddressSearch[A](userAgent: Option[UserAgent], addressRecords: List[AddressRecord], postcode: Option[Postcode] = None, - posttown: Option[String] = None, uprn: Option[String] = None, filter: Option[String] = None)(implicit hc: HeaderCarrier): Unit = { +class Auditor @Inject() (auditConnector: AuditConnector)(implicit + ec: ExecutionContext +) { + def auditAddressSearch[A]( + userAgent: Option[UserAgent], + addressRecords: List[AddressRecord], + postcode: Option[Postcode] = None, + posttown: Option[String] = None, + uprn: Option[String] = None, + filter: Option[String] = None + )(implicit hc: HeaderCarrier): Unit = { if (addressRecords.nonEmpty) { - val auditEventRequestDetails = AddressSearchAuditEventRequestDetails(postcode.map(_.toString), posttown, uprn, filter) + val auditEventRequestDetails = AddressSearchAuditEventRequestDetails( + postcode.map(_.toString), + posttown, + uprn, + filter + ) val addressSearchAuditEventMatchedAddresses = addressRecords.map { ma => AddressSearchAuditEventMatchedAddress( ma.uprn.map(_.toString).getOrElse(""), @@ -45,23 +65,34 @@ class Auditor @Inject()(auditConnector: AuditConnector)(implicit ec: ExecutionCo ma.poBox, ma.address.postcode, ma.address.subdivision, - ma.address.country) + ma.address.country + ) } - auditConnector.sendExplicitAudit("AddressSearch", - AddressSearchAuditEvent(userAgent.map(_.unwrap), + auditConnector.sendExplicitAudit( + "AddressSearch", + AddressSearchAuditEvent( + userAgent.map(_.unwrap), auditEventRequestDetails, addressRecords.length, - addressSearchAuditEventMatchedAddresses)) + addressSearchAuditEventMatchedAddresses + ) + ) } } - def auditNonUKAddressSearch[A](userAgent: Option[UserAgent], nonUKAddresses: List[NonUKAddress], country: String, - filter: Option[String] = None)(implicit hc: HeaderCarrier): Unit = { + def auditNonUKAddressSearch[A]( + userAgent: Option[UserAgent], + nonUKAddresses: List[NonUKAddress], + country: String, + filter: Option[String] = None + )(implicit hc: HeaderCarrier): Unit = { if (nonUKAddresses.nonEmpty) { - auditConnector.sendExplicitAudit("NonUKAddressSearch", - NonUKAddressSearchAuditEvent(userAgent.map(_.unwrap), + auditConnector.sendExplicitAudit( + "NonUKAddressSearch", + NonUKAddressSearchAuditEvent( + userAgent.map(_.unwrap), NonUKAddressSearchAuditEventRequestDetails(filter), nonUKAddresses.length, nonUKAddresses.map { ma => @@ -74,8 +105,11 @@ class Auditor @Inject()(auditConnector: AuditConnector)(implicit ec: ExecutionCo ma.district, ma.region, ma.postcode, - country) - })) + country + ) + } + ) + ) } } } diff --git a/app/config/AppConfig.scala b/app/config/AppConfig.scala index 06721ae..385bcbd 100644 --- a/app/config/AppConfig.scala +++ b/app/config/AppConfig.scala @@ -25,7 +25,10 @@ import java.util.Base64 import javax.inject.Singleton @Singleton -class AppConfig @Inject()(val config: Configuration, servicesConfig: ServicesConfig) { +class AppConfig @Inject() ( + val config: Configuration, + servicesConfig: ServicesConfig +) { val appName = config.get[String]("appName") val addressSearchApiBaseUrl = servicesConfig.baseUrl("address-search-api") @@ -40,14 +43,19 @@ class AppConfig @Inject()(val config: Configuration, servicesConfig: ServicesCon private def createAuth = AppConfig.createAuth( config.get[String]("appName"), - servicesConfig.getConfString("addressSearchApiAuthToken", "invalid-token")) + servicesConfig.getConfString("addressSearchApiAuthToken", "invalid-token") + ) def isCipPaasDbEnabled: Boolean = - config.getOptional[String]("cip-address-lookup-rds.enabled").getOrElse("false").toBoolean + config + .getOptional[String]("cip-address-lookup-rds.enabled") + .getOrElse("false") + .toBoolean } object AppConfig { - def createAuth(appName: String, authToken: String): String = Base64.getEncoder.encodeToString( - s"$appName:$authToken".getBytes(StandardCharsets.UTF_8) - ) + def createAuth(appName: String, authToken: String): String = + Base64.getEncoder.encodeToString( + s"$appName:$authToken".getBytes(StandardCharsets.UTF_8) + ) } diff --git a/app/config/InvalidJsonErrorHandler.scala b/app/config/InvalidJsonErrorHandler.scala index ae500fb..9cf885d 100644 --- a/app/config/InvalidJsonErrorHandler.scala +++ b/app/config/InvalidJsonErrorHandler.scala @@ -30,52 +30,76 @@ import uk.gov.hmrc.play.bootstrap.config.HttpAuditEvent import javax.inject.Inject import scala.concurrent.{ExecutionContext, Future} -class InvalidJsonErrorHandler @Inject()( - auditConnector: AuditConnector, httpAuditEvent: HttpAuditEvent, configuration: Configuration - )(implicit ec: ExecutionContext) - extends uk.gov.hmrc.play.bootstrap.backend.http.JsonErrorHandler(auditConnector, httpAuditEvent, configuration) { +class InvalidJsonErrorHandler @Inject() ( + auditConnector: AuditConnector, + httpAuditEvent: HttpAuditEvent, + configuration: Configuration +)(implicit ec: ExecutionContext) + extends uk.gov.hmrc.play.bootstrap.backend.http.JsonErrorHandler( + auditConnector, + httpAuditEvent, + configuration + ) { import httpAuditEvent._ - override def onClientError(request: RequestHeader, statusCode: Int, message: String): Future[Result] = { + override def onClientError( + request: RequestHeader, + statusCode: Int, + message: String + ): Future[Result] = { implicit val headerCarrier: HeaderCarrier = hc(request) val result = statusCode match { case NOT_FOUND => auditConnector.sendEvent( dataEvent( - eventType = "ResourceNotFound", + eventType = "ResourceNotFound", transactionName = "Resource Endpoint Not Found", - request = request, - detail = Map.empty + request = request, + detail = Map.empty + ) + ) + NotFound( + toJson( + ErrorResponse( + NOT_FOUND, + "URI not found", + requested = Some(request.path) + ) ) ) - NotFound(toJson(ErrorResponse(NOT_FOUND, "URI not found", requested = Some(request.path)))) case BAD_REQUEST => auditConnector.sendEvent( dataEvent( - eventType = "ServerValidationError", + eventType = "ServerValidationError", transactionName = "Request bad format exception", - request = request, - detail = Map.empty + request = request, + detail = Map.empty ) ) def constructErrorMessage(input: String): String = { - val unrecognisedTokenJsonError = "^Invalid Json: Unrecognized token '(.*)':.*".r - val invalidJson = "^(?s)Invalid Json:.*".r - val jsonValidationError = "^Json validation error.*".r - val booleanParsingError = "^Cannot parse parameter .* as Boolean: should be true, false, 0 or 1$".r - val missingParameterError = "^Missing parameter:.*".r - val characterParseError = "^Cannot parse parameter .* with value '(.*)' as Char: .* must be exactly one digit in length.$".r - val parameterParseError = "^Cannot parse parameter .* as .*: For input string: \"(.*)\"$".r + val unrecognisedTokenJsonError = + "^Invalid Json: Unrecognized token '(.*)':.*".r + val invalidJson = "^(?s)Invalid Json:.*".r + val jsonValidationError = "^Json validation error.*".r + val booleanParsingError = + "^Cannot parse parameter .* as Boolean: should be true, false, 0 or 1$".r + val missingParameterError = "^Missing parameter:.*".r + val characterParseError = + "^Cannot parse parameter .* with value '(.*)' as Char: .* must be exactly one digit in length.$".r + val parameterParseError = + "^Cannot parse parameter .* as .*: For input string: \"(.*)\"$".r input match { - case unrecognisedTokenJsonError(toBeRedacted) => input.replace(toBeRedacted, "REDACTED") - case invalidJson() - | jsonValidationError() - | booleanParsingError() - | missingParameterError() => filterJsonExceptionMsg(input) - case characterParseError(toBeRedacted) => input.replace(toBeRedacted, "REDACTED") - case parameterParseError(toBeRedacted) => input.replace(toBeRedacted, "REDACTED") - case _ => "bad request, cause: REDACTED" + case unrecognisedTokenJsonError(toBeRedacted) => + input.replace(toBeRedacted, "REDACTED") + case invalidJson() | jsonValidationError() | booleanParsingError() | + missingParameterError() => + filterJsonExceptionMsg(input) + case characterParseError(toBeRedacted) => + input.replace(toBeRedacted, "REDACTED") + case parameterParseError(toBeRedacted) => + input.replace(toBeRedacted, "REDACTED") + case _ => "bad request, cause: REDACTED" } } val msg = @@ -87,10 +111,10 @@ class InvalidJsonErrorHandler @Inject()( case _ => auditConnector.sendEvent( dataEvent( - eventType = "ClientError", + eventType = "ClientError", transactionName = s"A client error occurred, status: $statusCode", - request = request, - detail = Map.empty + request = request, + detail = Map.empty ) ) @@ -106,7 +130,7 @@ class InvalidJsonErrorHandler @Inject()( private def filterJsonExceptionMsg(msg: String): String = { msg.indexOf("at [Source") match { case -1 => msg - case x => msg.substring(0, x) + case x => msg.substring(0, x) } } } diff --git a/app/connectors/DownstreamConnector.scala b/app/connectors/DownstreamConnector.scala index e3858ad..50d9eb1 100644 --- a/app/connectors/DownstreamConnector.scala +++ b/app/connectors/DownstreamConnector.scala @@ -17,7 +17,12 @@ package connectors import play.api.Logging -import play.api.http.HeaderNames.{AUTHORIZATION, CONTENT_LENGTH, CONTENT_TYPE, HOST} +import play.api.http.HeaderNames.{ + AUTHORIZATION, + CONTENT_LENGTH, + CONTENT_TYPE, + HOST +} import play.api.http.{HeaderNames, HttpEntity, MimeTypes} import play.api.libs.json.JsValue import play.api.mvc.Results.{BadGateway, InternalServerError} @@ -28,56 +33,88 @@ import javax.inject.{Inject, Singleton} import scala.concurrent.{ExecutionContext, Future} @Singleton -class DownstreamConnector @Inject()(httpClient: HttpClient) extends Logging { - def forward(request: Request[JsValue], url: String, authToken: String)(implicit ec: ExecutionContext): Future[Result] = { +class DownstreamConnector @Inject() (httpClient: HttpClient) extends Logging { + def forward(request: Request[JsValue], url: String, authToken: String)( + implicit ec: ExecutionContext + ): Future[Result] = { import uk.gov.hmrc.http.HttpReads.Implicits.readRaw - implicit val hc: HeaderCarrier = DownstreamConnector.overrideHeaderCarrier(authToken) - val onwardHeaders = request.headers.remove(CONTENT_LENGTH, HOST, AUTHORIZATION).headers + implicit val hc: HeaderCarrier = + DownstreamConnector.overrideHeaderCarrier(authToken) + val onwardHeaders = + request.headers.remove(CONTENT_LENGTH, HOST, AUTHORIZATION).headers logger.info(s"Forwarding to downstream url: $url") // TODO: Check if the context path is present - if so remove before forwarding val called = request.method match { case "POST" => - httpClient.POST[Option[JsValue], HttpResponse](url = url, body = Some(request.body), onwardHeaders) - case "GET" => + httpClient.POST[Option[JsValue], HttpResponse]( + url = url, + body = Some(request.body), + onwardHeaders + ) + case "GET" => httpClient.GET[HttpResponse](url = url, onwardHeaders) } try { - called.map { response: HttpResponse => - val returnHeaders = response.headers - .filterNot { case (n, _) => n == CONTENT_TYPE || n == CONTENT_LENGTH } - .view.mapValues(x => x.mkString).toMap - Result( - ResponseHeader(response.status, returnHeaders), - HttpEntity.Streamed(response.bodyAsSource, None, response.header(CONTENT_TYPE))) - }.recoverWith { case t: Throwable => - logger.error(s"Downstream call failed with ${t.getMessage}") - Future.successful(BadGateway("{\"code\": \"REQUEST_DOWNSTREAM\", \"desc\": \"An issue occurred when the downstream service tried to handle the request\"}").as(MimeTypes.JSON)) - } + called + .map { response: HttpResponse => + val returnHeaders = response.headers + .filterNot { case (n, _) => + n == CONTENT_TYPE || n == CONTENT_LENGTH + } + .view + .mapValues(x => x.mkString) + .toMap + Result( + ResponseHeader(response.status, returnHeaders), + HttpEntity.Streamed( + response.bodyAsSource, + None, + response.header(CONTENT_TYPE) + ) + ) + } + .recoverWith { case t: Throwable => + logger.error(s"Downstream call failed with ${t.getMessage}") + Future.successful( + BadGateway( + "{\"code\": \"REQUEST_DOWNSTREAM\", \"desc\": \"An issue occurred when the downstream service tried to handle the request\"}" + ).as(MimeTypes.JSON) + ) + } } catch { case t: Throwable => logger.error(s"Call to search service failed with ${t.getMessage}") - Future.successful(InternalServerError("{\"code\": \"REQUEST_FORWARDING\", \"desc\": \"An issue occurred when forwarding the request to the downstream service\"}").as(MimeTypes.JSON)) + Future.successful( + InternalServerError( + "{\"code\": \"REQUEST_FORWARDING\", \"desc\": \"An issue occurred when forwarding the request to the downstream service\"}" + ).as(MimeTypes.JSON) + ) } } - def checkConnectivity(url: String, authToken: String)(implicit ec: ExecutionContext): Future[Boolean] = { + def checkConnectivity(url: String, authToken: String)(implicit + ec: ExecutionContext + ): Future[Boolean] = { import uk.gov.hmrc.http.HttpReads.Implicits.readRaw - implicit val hc: HeaderCarrier = DownstreamConnector.overrideHeaderCarrier(authToken) + implicit val hc: HeaderCarrier = + DownstreamConnector.overrideHeaderCarrier(authToken) try { - httpClient.GET[HttpResponse](url = url).map { - case response if response.status > 400 => false - case response if response.status / 100 == 5 => false - case _ => true - }.recoverWith { case t: Throwable => - Future.successful(false) - } - } - catch { + httpClient + .GET[HttpResponse](url = url) + .map { + case response if response.status > 400 => false + case response if response.status / 100 == 5 => false + case _ => true + } + .recoverWith { case t: Throwable => + Future.successful(false) + } + } catch { case t: Throwable => Future.successful(false) } } diff --git a/app/controllers/AddressSearchController.scala b/app/controllers/AddressSearchController.scala index 5da4b9a..522489a 100644 --- a/app/controllers/AddressSearchController.scala +++ b/app/controllers/AddressSearchController.scala @@ -36,107 +36,209 @@ import uk.gov.hmrc.play.http.HeaderCarrierConverter import javax.inject.Inject import scala.concurrent.{ExecutionContext, Future} -class AddressSearchController @Inject()(connector: DownstreamConnector, auditor: Auditor, cc: ControllerComponents, val configHelper: AppConfig)(implicit ec: ExecutionContext) - extends BackendController(cc) with Logging with AccessChecker { +class AddressSearchController @Inject() ( + connector: DownstreamConnector, + auditor: Auditor, + cc: ControllerComponents, + val configHelper: AppConfig +)(implicit ec: ExecutionContext) + extends BackendController(cc) + with Logging + with AccessChecker { private val actorSystem = ActorSystem("AddressSearchController") - private implicit val materializer: Materializer = Materializer.createMaterializer(actorSystem) + private implicit val materializer: Materializer = + Materializer.createMaterializer(actorSystem) - def searchByPostcode(): Action[LookupByPostcodeRequest] = accessCheckedAction(parse.json[LookupByPostcodeRequest]) { - implicit request: Request[LookupByPostcodeRequest] => - searchByPostcode(request) + def searchByPostcode(): Action[LookupByPostcodeRequest] = accessCheckedAction( + parse.json[LookupByPostcodeRequest] + ) { implicit request: Request[LookupByPostcodeRequest] => + searchByPostcode(request) } - def searchByUprn(): Action[LookupByUprnRequest] = accessCheckedAction(parse.json[LookupByUprnRequest]) { - implicit request: Request[LookupByUprnRequest] => - searchByUprn(request) + def searchByUprn(): Action[LookupByUprnRequest] = accessCheckedAction( + parse.json[LookupByUprnRequest] + ) { implicit request: Request[LookupByUprnRequest] => + searchByUprn(request) } - def searchByPostTown(): Action[LookupByPostTownRequest] = accessCheckedAction(parse.json[LookupByPostTownRequest]) { - implicit request: Request[LookupByPostTownRequest] => - searchByTown(request) + def searchByPostTown(): Action[LookupByPostTownRequest] = accessCheckedAction( + parse.json[LookupByPostTownRequest] + ) { implicit request: Request[LookupByPostTownRequest] => + searchByTown(request) } def supportedCountries(): Action[AnyContent] = Action.async { request: Request[AnyContent] => - forwardIfAllowed[JsValue, JsValue](request.map(r => JsObject.empty), _ => ()) + forwardIfAllowed[JsValue, JsValue]( + request.map(r => JsObject.empty), + _ => () + ) } - def searchByCountry(countryCode: String): Action[LookupByCountryFilterRequest] = accessCheckedAction(parse.json[LookupByCountryFilterRequest]) { - implicit request: Request[LookupByCountryFilterRequest] => - val newRequest: Request[LookupByCountryRequest] = - request.withTarget(RequestTarget("/country/lookup", "/country/lookup", request.queryString)) - .withBody(addCountryTo(request.body, countryCode.toLowerCase)) - - - searchByCountry(newRequest) + def searchByCountry( + countryCode: String + ): Action[LookupByCountryFilterRequest] = accessCheckedAction( + parse.json[LookupByCountryFilterRequest] + ) { implicit request: Request[LookupByCountryFilterRequest] => + val newRequest: Request[LookupByCountryRequest] = + request + .withTarget( + RequestTarget( + "/country/lookup", + "/country/lookup", + request.queryString + ) + ) + .withBody(addCountryTo(request.body, countryCode.toLowerCase)) + + searchByCountry(newRequest) } - private[controllers] def searchByUprn(request: Request[LookupByUprnRequest])(implicit hc: HeaderCarrier, userAgent: Option[UserAgent]): Future[Result] = { + private[controllers] def searchByUprn( + request: Request[LookupByUprnRequest] + )(implicit + hc: HeaderCarrier, + userAgent: Option[UserAgent] + ): Future[Result] = { import model.address.AddressRecord.formats._ - forwardIfAllowed[LookupByUprnRequest, List[AddressRecord]](request, - addresses => auditor.auditAddressSearch(userAgent, addresses, uprn = Some(request.body.uprn)) + forwardIfAllowed[LookupByUprnRequest, List[AddressRecord]]( + request, + addresses => + auditor.auditAddressSearch( + userAgent, + addresses, + uprn = Some(request.body.uprn) + ) ) } - private[controllers] def searchByPostcode[A](request: Request[LookupByPostcodeRequest])(implicit hc: HeaderCarrier, userAgent: Option[UserAgent]): Future[Result] = { + private[controllers] def searchByPostcode[A]( + request: Request[LookupByPostcodeRequest] + )(implicit + hc: HeaderCarrier, + userAgent: Option[UserAgent] + ): Future[Result] = { import model.address.AddressRecord.formats._ val postcode: LookupByPostcodeRequest = request.body - forwardIfAllowed[LookupByPostcodeRequest, List[AddressRecord]](request, - addresses => auditor.auditAddressSearch(userAgent, addresses, postcode = Some(postcode.postcode), filter = postcode.filter)) + forwardIfAllowed[LookupByPostcodeRequest, List[AddressRecord]]( + request, + addresses => + auditor.auditAddressSearch( + userAgent, + addresses, + postcode = Some(postcode.postcode), + filter = postcode.filter + ) + ) } - private[controllers] def searchByTown[A](request: Request[LookupByPostTownRequest])(implicit hc: HeaderCarrier, userAgent: Option[UserAgent]): Future[Result] = { + private[controllers] def searchByTown[A]( + request: Request[LookupByPostTownRequest] + )(implicit + hc: HeaderCarrier, + userAgent: Option[UserAgent] + ): Future[Result] = { import model.address.AddressRecord.formats._ val posttown: LookupByPostTownRequest = request.body - forwardIfAllowed[LookupByPostTownRequest, List[AddressRecord]](request, - addresses => auditor.auditAddressSearch(userAgent, addresses, posttown = Some(posttown.posttown.toUpperCase), filter = posttown.filter)) + forwardIfAllowed[LookupByPostTownRequest, List[AddressRecord]]( + request, + addresses => + auditor.auditAddressSearch( + userAgent, + addresses, + posttown = Some(posttown.posttown.toUpperCase), + filter = posttown.filter + ) + ) } - private[controllers] def searchByCountry[A](request: Request[LookupByCountryRequest])(implicit hc: HeaderCarrier, userAgent: Option[UserAgent]): Future[Result] = { + private[controllers] def searchByCountry[A]( + request: Request[LookupByCountryRequest] + )(implicit + hc: HeaderCarrier, + userAgent: Option[UserAgent] + ): Future[Result] = { import model.address.NonUKAddress._ val country: LookupByCountryRequest = request.body - forwardIfAllowed[LookupByCountryRequest, List[NonUKAddress]](request, - addresses => auditor.auditNonUKAddressSearch(userAgent, country = country.country, filter = Option(country.filter), nonUKAddresses = addresses)) + forwardIfAllowed[LookupByCountryRequest, List[NonUKAddress]]( + request, + addresses => + auditor.auditNonUKAddressSearch( + userAgent, + country = country.country, + filter = Option(country.filter), + nonUKAddresses = addresses + ) + ) } - private def addCountryTo(body: LookupByCountryFilterRequest, country: String): LookupByCountryRequest = { + private def addCountryTo( + body: LookupByCountryFilterRequest, + country: String + ): LookupByCountryRequest = { LookupByCountryRequest(country, body.filter) } - private def url(path: String) = s"${configHelper.addressSearchApiBaseUrl}$path" + private def url(path: String) = + s"${configHelper.addressSearchApiBaseUrl}$path" - private def forwardIfAllowed[Req: Writes, Resp:Reads](request: Request[Req], auditFn: Resp => Unit): Future[Result] = { - val newHeadersMap = request.headers.toSimpleMap ++ Map(HeaderNames.CONTENT_TYPE -> MimeTypes.JSON) + private def forwardIfAllowed[Req: Writes, Resp: Reads]( + request: Request[Req], + auditFn: Resp => Unit + ): Future[Result] = { + val newHeadersMap = request.headers.toSimpleMap ++ Map( + HeaderNames.CONTENT_TYPE -> MimeTypes.JSON + ) val jsonRequest = request.withHeaders(Headers(newHeadersMap.toSeq: _*)) - connector.forward(jsonRequest.map((r: Req) => Json.toJson(r)), url(jsonRequest.target.uri.toString), configHelper.addressSearchApiAuthToken) + connector + .forward( + jsonRequest.map((r: Req) => Json.toJson(r)), + url(jsonRequest.target.uri.toString), + configHelper.addressSearchApiAuthToken + ) .flatMap(res => res.body.consumeData.map(d => res.header.status -> d)) .map { case (s, bs) => s -> bs.utf8String } .map { case (s, res) => s -> Json.parse(res) } .map { - case (OK, js) => + case (OK, js) => auditFn(Json.fromJson[Resp](js).get) Ok(js) case (NOT_FOUND, err) => NotFound(err) case (BAD_REQUEST, err) => BadRequest(err) - case (FORBIDDEN, err) => Forbidden(err) + case (FORBIDDEN, err) => Forbidden(err) } } - implicit def requestToHeaderCarrier[T](implicit request: Request[T]): HeaderCarrier = + implicit def requestToHeaderCarrier[T](implicit + request: Request[T] + ): HeaderCarrier = HeaderCarrierConverter.fromRequest(request) - implicit def requestToUserAgent[T](implicit request: Request[T]): Option[UserAgent] = + implicit def requestToUserAgent[T](implicit + request: Request[T] + ): Option[UserAgent] = UserAgent(request) - connector.checkConnectivity(url("/ping/ping"), configHelper.addressSearchApiAuthToken).map { - case true => logger.warn("Downstream connectivity to address-search-api service successfully established") - case _ => logger.error("Downstream connectivity check to address-search-api service FAILED") - } + connector + .checkConnectivity( + url("/ping/ping"), + configHelper.addressSearchApiAuthToken + ) + .map { + case true => + logger.warn( + "Downstream connectivity to address-search-api service successfully established" + ) + case _ => + logger.error( + "Downstream connectivity check to address-search-api service FAILED" + ) + } } diff --git a/app/model/AddressSearchAuditEvent.scala b/app/model/AddressSearchAuditEvent.scala index 6f4cc5a..c068d99 100644 --- a/app/model/AddressSearchAuditEvent.scala +++ b/app/model/AddressSearchAuditEvent.scala @@ -1,54 +1,64 @@ -/* - * 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 model.address.{Country, LocalCustodian} -import play.api.libs.json._ -import play.api.libs.json.Json - -case class AddressSearchAuditEventMatchedAddress(uprn: String, - parentUprn: Option[Long], - usrn: Option[Long], - organisation: Option[String], - lines: Seq[String], - town: String, - localCustodian: Option[LocalCustodian], - location: Option[Seq[BigDecimal]], - administrativeArea: Option[String], - poBox: Option[String], - postCode: String, - subDivision: Option[Country], - country: Country) - -case class AddressSearchAuditEventRequestDetails(postcode: Option[String] = None, - postTown: Option[String] = None, - uprn: Option[String] = None, - filter: Option[String] = None) - -case class AddressSearchAuditEvent(userAgent: Option[String], - request: AddressSearchAuditEventRequestDetails, - numberOfAddressFound: Int, - matchedAddresses: Seq[AddressSearchAuditEventMatchedAddress]) - -object AddressSearchAuditEvent { - import Country.formats._ - import LocalCustodian.formats._ - - implicit def requestDetailsWrites: Writes[AddressSearchAuditEventRequestDetails] = Json.writes[AddressSearchAuditEventRequestDetails] - implicit def addressWrites: Writes[AddressSearchAuditEventMatchedAddress] = Json.writes[AddressSearchAuditEventMatchedAddress] - implicit def writes: Writes[AddressSearchAuditEvent] = Json.writes[AddressSearchAuditEvent] -} +/* + * 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 model.address.{Country, LocalCustodian} +import play.api.libs.json._ +import play.api.libs.json.Json + +case class AddressSearchAuditEventMatchedAddress( + uprn: String, + parentUprn: Option[Long], + usrn: Option[Long], + organisation: Option[String], + lines: Seq[String], + town: String, + localCustodian: Option[LocalCustodian], + location: Option[Seq[BigDecimal]], + administrativeArea: Option[String], + poBox: Option[String], + postCode: String, + subDivision: Option[Country], + country: Country +) + +case class AddressSearchAuditEventRequestDetails( + postcode: Option[String] = None, + postTown: Option[String] = None, + uprn: Option[String] = None, + filter: Option[String] = None +) + +case class AddressSearchAuditEvent( + userAgent: Option[String], + request: AddressSearchAuditEventRequestDetails, + numberOfAddressFound: Int, + matchedAddresses: Seq[AddressSearchAuditEventMatchedAddress] +) + +object AddressSearchAuditEvent { + import Country.formats._ + import LocalCustodian.formats._ + + implicit def requestDetailsWrites + : Writes[AddressSearchAuditEventRequestDetails] = + Json.writes[AddressSearchAuditEventRequestDetails] + implicit def addressWrites: Writes[AddressSearchAuditEventMatchedAddress] = + Json.writes[AddressSearchAuditEventMatchedAddress] + implicit def writes: Writes[AddressSearchAuditEvent] = + Json.writes[AddressSearchAuditEvent] +} diff --git a/app/model/NonUKAddressSearchAuditEvent.scala b/app/model/NonUKAddressSearchAuditEvent.scala index 853b58e..961697c 100644 --- a/app/model/NonUKAddressSearchAuditEvent.scala +++ b/app/model/NonUKAddressSearchAuditEvent.scala @@ -19,20 +19,37 @@ package model import play.api.libs.json._ import play.api.libs.json.Json -case class NonUKAddressSearchAuditEventMatchedAddress(id: Option[String], number: Option[String], street: Option[String], unit: Option[String], city: Option[String], district: Option[String], region: Option[String], postCode: Option[String], country: String) +case class NonUKAddressSearchAuditEventMatchedAddress( + id: Option[String], + number: Option[String], + street: Option[String], + unit: Option[String], + city: Option[String], + district: Option[String], + region: Option[String], + postCode: Option[String], + country: String +) case class NonUKAddressSearchAuditEventRequestDetails(filter: Option[String]) -case class NonUKAddressSearchAuditEvent(userAgent: Option[String], - request: NonUKAddressSearchAuditEventRequestDetails, - numberOfAddressFound: Int, - matchedAddresses: Seq[NonUKAddressSearchAuditEventMatchedAddress]) +case class NonUKAddressSearchAuditEvent( + userAgent: Option[String], + request: NonUKAddressSearchAuditEventRequestDetails, + numberOfAddressFound: Int, + matchedAddresses: Seq[NonUKAddressSearchAuditEventMatchedAddress] +) object NonUKAddressSearchAuditEvent { - implicit def requestDetailsWrites: Writes[NonUKAddressSearchAuditEventRequestDetails] = Json.writes[NonUKAddressSearchAuditEventRequestDetails] + implicit def requestDetailsWrites + : Writes[NonUKAddressSearchAuditEventRequestDetails] = + Json.writes[NonUKAddressSearchAuditEventRequestDetails] - implicit def addressWrites: Writes[NonUKAddressSearchAuditEventMatchedAddress] = Json.writes[NonUKAddressSearchAuditEventMatchedAddress] + implicit def addressWrites + : Writes[NonUKAddressSearchAuditEventMatchedAddress] = + Json.writes[NonUKAddressSearchAuditEventMatchedAddress] - implicit def writes: Writes[NonUKAddressSearchAuditEvent] = Json.writes[NonUKAddressSearchAuditEvent] + implicit def writes: Writes[NonUKAddressSearchAuditEvent] = + Json.writes[NonUKAddressSearchAuditEvent] } diff --git a/app/model/address/Address.scala b/app/model/address/Address.scala index 0433324..52150ec 100644 --- a/app/model/address/Address.scala +++ b/app/model/address/Address.scala @@ -22,21 +22,23 @@ import play.api.libs.json.{JsPath, Reads, Writes} import java.util.regex.Pattern -/** - * Address typically represents a postal address. - * For UK addresses, 'town' will always be present. - * For non-UK addresses, 'town' may be absent and there may be an extra line instead. +/** Address typically represents a postal address. For UK addresses, 'town' will + * always be present. For non-UK addresses, 'town' may be absent and there may + * be an extra line instead. */ -case class Address(lines: List[String], - town: String, - postcode: String, - subdivision: Option[Country], - country: Country) { +case class Address( + lines: List[String], + town: String, + postcode: String, + subdivision: Option[Country], + country: Country +) { import Address._ @JsonIgnore // needed because the name starts 'is...' - def isValid: Boolean = lines.nonEmpty && lines.size <= (if (town.isEmpty) 4 else 3) + def isValid: Boolean = + lines.nonEmpty && lines.size <= (if (town.isEmpty) 4 else 3) def nonEmptyFields: List[String] = lines ::: List(town) ::: List(postcode) @@ -58,10 +60,15 @@ case class Address(lines: List[String], def longestLineLength: Int = nonEmptyFields.map(_.length).max def truncatedAddress(maxLen: Int = maxLineLength): Address = - Address(lines.map(limit(_, maxLen)), limit(town, maxLen), postcode, subdivision, country) + Address( + lines.map(limit(_, maxLen)), + limit(town, maxLen), + postcode, + subdivision, + country + ) } - object Address { val maxLineLength = 35 val danglingLetter: Pattern = Pattern.compile(".* [A-Z0-9]$") @@ -77,26 +84,25 @@ object Address { s = s.substring(0, s.length - 2) } s - } - else s + } else s } object formats { import Country.formats._ - implicit val addressReads: Reads[Address] = ( - (JsPath \ "lines").read[List[String]] and - (JsPath \ "town").read[String] and - (JsPath \ "postcode").read[String] and - (JsPath \ "subdivision").readNullable[Country] and - (JsPath \ "country").read[Country]) (Address.apply _) + implicit val addressReads: Reads[Address] = + ((JsPath \ "lines").read[List[String]] and + (JsPath \ "town").read[String] and + (JsPath \ "postcode").read[String] and + (JsPath \ "subdivision").readNullable[Country] and + (JsPath \ "country").read[Country])(Address.apply _) - implicit val addressWrites: Writes[Address] = ( - (JsPath \ "lines").write[Seq[String]] and - (JsPath \ "town").write[String] and - (JsPath \ "postcode").write[String] and - (JsPath \ "subdivision").writeNullable[Country] and - (JsPath \ "country").write[Country]) (unlift(Address.unapply)) + implicit val addressWrites: Writes[Address] = + ((JsPath \ "lines").write[Seq[String]] and + (JsPath \ "town").write[String] and + (JsPath \ "postcode").write[String] and + (JsPath \ "subdivision").writeNullable[Country] and + (JsPath \ "country").write[Country])(unlift(Address.unapply)) } -} \ No newline at end of file +} diff --git a/app/model/address/AddressRecord.scala b/app/model/address/AddressRecord.scala index 7e57468..20103c0 100644 --- a/app/model/address/AddressRecord.scala +++ b/app/model/address/AddressRecord.scala @@ -20,40 +20,39 @@ import com.fasterxml.jackson.annotation.JsonIgnore import play.api.libs.functional.syntax._ import play.api.libs.json.{JsPath, Reads, Writes} - case class LocalCustodian(code: Int, name: String) object LocalCustodian { object formats { - implicit val localCustodianReads: Reads[LocalCustodian] = ( - (JsPath \ "code").read[Int] and - (JsPath \ "name").read[String]) (LocalCustodian.apply _) + implicit val localCustodianReads: Reads[LocalCustodian] = + ((JsPath \ "code").read[Int] and + (JsPath \ "name").read[String])(LocalCustodian.apply _) - implicit val localCustodianWrites: Writes[LocalCustodian] = ( - (JsPath \ "code").write[Int] and - (JsPath \ "name").write[String]) (unlift(LocalCustodian.unapply)) + implicit val localCustodianWrites: Writes[LocalCustodian] = + ((JsPath \ "code").write[Int] and + (JsPath \ "name").write[String])(unlift(LocalCustodian.unapply)) } } - -/** - * Represents one address record. Arrays of these are returned from the address-lookup microservice. +/** Represents one address record. Arrays of these are returned from the + * address-lookup microservice. */ case class AddressRecord( - id: String, - uprn: Option[Long], - parentUprn: Option[Long], - usrn: Option[Long], - organisation: Option[String], - address: Address, - // ISO639-1 code, e.g. 'en' for English - // see https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes - language: String, - localCustodian: Option[LocalCustodian], - location: Option[Seq[BigDecimal]], - administrativeArea: Option[String] = None, - poBox: Option[String] = None) { + id: String, + uprn: Option[Long], + parentUprn: Option[Long], + usrn: Option[Long], + organisation: Option[String], + address: Address, + // ISO639-1 code, e.g. 'en' for English + // see https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes + language: String, + localCustodian: Option[LocalCustodian], + location: Option[Seq[BigDecimal]], + administrativeArea: Option[String] = None, + poBox: Option[String] = None +) { require(location.isEmpty || location.get.size == 2, location.get) @@ -71,32 +70,32 @@ object AddressRecord { import LocalCustodian.formats._ implicit val addressRecordReads: Reads[AddressRecord] = ( - (JsPath \ "id").read[String] and - (JsPath \ "uprn").readNullable[Long] and - (JsPath \ "parentUprn").readNullable[Long] and - (JsPath \ "usrn").readNullable[Long] and - (JsPath \ "organisation").readNullable[String] and - (JsPath \ "address").read[Address] and - (JsPath \ "language").read[String] and - (JsPath \ "localCustodian").readNullable[LocalCustodian] and - (JsPath \ "location").readNullable[Seq[BigDecimal]] and - (JsPath \ "administrativeArea").readNullable[String] and - (JsPath \ "poBox").readNullable[String] - ) (AddressRecord.apply _) + (JsPath \ "id").read[String] and + (JsPath \ "uprn").readNullable[Long] and + (JsPath \ "parentUprn").readNullable[Long] and + (JsPath \ "usrn").readNullable[Long] and + (JsPath \ "organisation").readNullable[String] and + (JsPath \ "address").read[Address] and + (JsPath \ "language").read[String] and + (JsPath \ "localCustodian").readNullable[LocalCustodian] and + (JsPath \ "location").readNullable[Seq[BigDecimal]] and + (JsPath \ "administrativeArea").readNullable[String] and + (JsPath \ "poBox").readNullable[String] + )(AddressRecord.apply _) implicit val addressRecordWrites: Writes[AddressRecord] = ( - (JsPath \ "id").write[String] and - (JsPath \ "uprn").writeNullable[Long] and - (JsPath \ "parentUprn").writeNullable[Long] and - (JsPath \ "usrn").writeNullable[Long] and - (JsPath \ "organisation").writeNullable[String] and - (JsPath \ "address").write[Address] and - (JsPath \ "language").write[String] and - (JsPath \ "localCustodian").writeNullable[LocalCustodian] and - (JsPath \ "location").writeNullable[Seq[BigDecimal]] and - (JsPath \ "administrativeArea").writeNullable[String] and - (JsPath \ "poBox").writeNullable[String] - ) (unlift(AddressRecord.unapply)) + (JsPath \ "id").write[String] and + (JsPath \ "uprn").writeNullable[Long] and + (JsPath \ "parentUprn").writeNullable[Long] and + (JsPath \ "usrn").writeNullable[Long] and + (JsPath \ "organisation").writeNullable[String] and + (JsPath \ "address").write[Address] and + (JsPath \ "language").write[String] and + (JsPath \ "localCustodian").writeNullable[LocalCustodian] and + (JsPath \ "location").writeNullable[Seq[BigDecimal]] and + (JsPath \ "administrativeArea").writeNullable[String] and + (JsPath \ "poBox").writeNullable[String] + )(unlift(AddressRecord.unapply)) } } diff --git a/app/model/address/Country.scala b/app/model/address/Country.scala index 6f73b6e..d67fa34 100644 --- a/app/model/address/Country.scala +++ b/app/model/address/Country.scala @@ -22,21 +22,21 @@ import play.api.libs.json.{JsPath, Json, Reads, Writes} /** Represents a country as per ISO3166. */ case class Country( - // ISO3166-1 or ISO3166-2 code, e.g. "GB" or "GB-ENG" (note that "GB" is the official - // code for UK although "UK" is a reserved synonym and may be used instead) - // See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 - // and https://en.wikipedia.org/wiki/ISO_3166-2:GB - code: String, - // The printable name for the country, e.g. "United Kingdom" - name: String) { -} - + // ISO3166-1 or ISO3166-2 code, e.g. "GB" or "GB-ENG" (note that "GB" is the official + // code for UK although "UK" is a reserved synonym and may be used instead) + // See https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + // and https://en.wikipedia.org/wiki/ISO_3166-2:GB + code: String, + // The printable name for the country, e.g. "United Kingdom" + name: String +) {} object Country { object formats { - implicit val countryReads: Reads[Country] = ( - (JsPath \ "code").read[String](minLength[String](2) keepAnd maxLength[String](6)) and - (JsPath \ "name").read[String]) (Country.apply _) + implicit val countryReads: Reads[Country] = ((JsPath \ "code").read[String]( + minLength[String](2) keepAnd maxLength[String](6) + ) and + (JsPath \ "name").read[String])(Country.apply _) implicit val countryWrites: Writes[Country] = Json.writes[Country] } @@ -44,7 +44,8 @@ object Country { @deprecated("GB is the official ISO code for UK", "") val UK = Country("UK", "United Kingdom") - val GB = Country("GB", "United Kingdom") // special case provided for in ISO-3166 + val GB = + Country("GB", "United Kingdom") // special case provided for in ISO-3166 val GG = Country("GG", "Guernsey") val IM = Country("IM", "Isle of Man") val JE = Country("JE", "Jersey") @@ -55,7 +56,8 @@ object Country { val Cymru = Country("GB-CYM", "Cymru") val NorthernIreland = Country("GB-NIR", "Northern Ireland") - private val all = List(UK, GB, GG, IM, JE, England, Scotland, Wales, Cymru, NorthernIreland) + private val all = + List(UK, GB, GG, IM, JE, England, Scotland, Wales, Cymru, NorthernIreland) def find(code: String): Option[Country] = all.find(_.code == code) diff --git a/app/model/address/Location.scala b/app/model/address/Location.scala index e5299b2..a1fde08 100644 --- a/app/model/address/Location.scala +++ b/app/model/address/Location.scala @@ -26,7 +26,8 @@ case class Location(latitude: BigDecimal, longitude: BigDecimal) { } object Location { - def apply(lat: String, long: String): Location = new Location(BigDecimal(lat), BigDecimal(long)) + def apply(lat: String, long: String): Location = + new Location(BigDecimal(lat), BigDecimal(long)) def apply(latlong: String): Location = { val seq = latlong.divide(',') diff --git a/app/model/address/NonUKAddress.scala b/app/model/address/NonUKAddress.scala index fb8dc52..2713524 100644 --- a/app/model/address/NonUKAddress.scala +++ b/app/model/address/NonUKAddress.scala @@ -18,7 +18,16 @@ package model.address import play.api.libs.json._ -case class NonUKAddress(id: Option[String], number: Option[String], street: Option[String], unit: Option[String], city: Option[String], district: Option[String], region: Option[String], postcode: Option[String]) +case class NonUKAddress( + id: Option[String], + number: Option[String], + street: Option[String], + unit: Option[String], + city: Option[String], + district: Option[String], + region: Option[String], + postcode: Option[String] +) object NonUKAddress { implicit val format: Format[NonUKAddress] = Json.format[NonUKAddress] diff --git a/app/model/address/Outcode.scala b/app/model/address/Outcode.scala index c93bb3c..a76cece 100644 --- a/app/model/address/Outcode.scala +++ b/app/model/address/Outcode.scala @@ -21,7 +21,6 @@ case class Outcode(area: String, district: String) { override lazy val toString = area + district } - object Outcode { private def doCleanupOutcode(p: String): Option[Outcode] = { diff --git a/app/model/address/Postcode.scala b/app/model/address/Postcode.scala index 2763bdc..44a646e 100644 --- a/app/model/address/Postcode.scala +++ b/app/model/address/Postcode.scala @@ -18,7 +18,12 @@ package model.address import java.util.regex.Pattern -case class Postcode(area: String, district: String, sector: String, unit: String) { +case class Postcode( + area: String, + district: String, + sector: String, + unit: String +) { def outcode: String = area + district def incode: String = sector + unit @@ -26,15 +31,14 @@ case class Postcode(area: String, district: String, sector: String, unit: String override lazy val toString = outcode + " " + incode } - object Postcode { // The basic syntax of a postcode (ignores the rules on valid letter ranges because they don't matter here). - private[address] val oPattern = Pattern.compile("^GIR|[A-Z]{1,2}[0-9][0-9A-Z]?$") + private[address] val oPattern = + Pattern.compile("^GIR|[A-Z]{1,2}[0-9][0-9A-Z]?$") private[address] val iPattern = Pattern.compile("^[0-9][A-Z]{2}$") - /** - * Performs normalisation and then checks the syntax, returning None if the string - * cannot represent a well-formed postcode. + /** Performs normalisation and then checks the syntax, returning None if the + * string cannot represent a well-formed postcode. */ def cleanupPostcode(p: String): Option[Postcode] = { if (p == null) None @@ -45,7 +49,6 @@ object Postcode { val norm = normalisePostcode(p) val space = norm.indexOf(' ') if (norm.length < 5) None - else if (space < 0) { val incodeLength = norm.length - 3 val out = norm.substring(0, incodeLength) @@ -79,13 +82,14 @@ object Postcode { } def unapply(postcode: Postcode): Option[String] = { - if(postcode == null) None + if (postcode == null) None else Some(postcode.toString) } def apply(outcode: String, incode: String): Postcode = { val (area, district) = - if (Character.isDigit(outcode(1))) (outcode.substring(0, 1), outcode.substring(1)) + if (Character.isDigit(outcode(1))) + (outcode.substring(0, 1), outcode.substring(1)) else (outcode.substring(0, 2), outcode.substring(2)) val sector = incode.substring(0, 1) val unit = incode.substring(1) diff --git a/app/model/request.scala b/app/model/request.scala index a39fe46..618833e 100644 --- a/app/model/request.scala +++ b/app/model/request.scala @@ -23,14 +23,16 @@ import play.api.libs.json.Reads._ import play.api.libs.json._ import play.api.mvc.Request - object request { - case class LookupByPostcodeRequest(postcode: Postcode, filter: Option[String] = None) + case class LookupByPostcodeRequest( + postcode: Postcode, + filter: Option[String] = None + ) object LookupByPostcodeRequest { implicit val postcodeReads: Reads[Postcode] = Reads[Postcode] { json => json.validate[String] match { - case e: JsError => e + case e: JsError => e case s: JsSuccess[String] => Postcode.cleanupPostcode(s.get) match { case pc if pc.isDefined => JsSuccess(pc.get) @@ -39,55 +41,61 @@ object request { } } - implicit val postcodeWrites: Writes[Postcode] = new Writes[Postcode]{ + implicit val postcodeWrites: Writes[Postcode] = new Writes[Postcode] { override def writes(o: Postcode): JsValue = JsString(o.toString) } implicit val reads: Reads[LookupByPostcodeRequest] = ( - (JsPath \ "postcode").read[Postcode] and - (JsPath \ "filter").readNullable[String].map(fo => - fo.flatMap(f => if(f.trim.isEmpty) None else Some(f)) - ) - ) ( - (pc, fo) => LookupByPostcodeRequest.apply(pc, fo)) - - implicit val writes: Writes[LookupByPostcodeRequest] = Json.writes[LookupByPostcodeRequest] + (JsPath \ "postcode").read[Postcode] and + (JsPath \ "filter") + .readNullable[String] + .map(fo => fo.flatMap(f => if (f.trim.isEmpty) None else Some(f))) + )((pc, fo) => LookupByPostcodeRequest.apply(pc, fo)) + + implicit val writes: Writes[LookupByPostcodeRequest] = + Json.writes[LookupByPostcodeRequest] } case class LookupByUprnRequest(uprn: String) object LookupByUprnRequest { - implicit val reads: Reads[LookupByUprnRequest] = Json.reads[LookupByUprnRequest] - implicit val writes: Writes[LookupByUprnRequest] = Json.writes[LookupByUprnRequest] + implicit val reads: Reads[LookupByUprnRequest] = + Json.reads[LookupByUprnRequest] + implicit val writes: Writes[LookupByUprnRequest] = + Json.writes[LookupByUprnRequest] } case class LookupByPostTownRequest(posttown: String, filter: Option[String]) object LookupByPostTownRequest { implicit val reads: Reads[LookupByPostTownRequest] = ( - (JsPath \ "posttown").read[String] and - (JsPath \ "filter").readNullable[String].map(fo => - fo.flatMap(f => if(f.trim.isEmpty) None else Some(f)) - ) - ) ( - (pt, fo) => LookupByPostTownRequest.apply(pt, fo)) - - implicit val writes: Writes[LookupByPostTownRequest] = Json.writes[LookupByPostTownRequest] + (JsPath \ "posttown").read[String] and + (JsPath \ "filter") + .readNullable[String] + .map(fo => fo.flatMap(f => if (f.trim.isEmpty) None else Some(f))) + )((pt, fo) => LookupByPostTownRequest.apply(pt, fo)) + + implicit val writes: Writes[LookupByPostTownRequest] = + Json.writes[LookupByPostTownRequest] } case class LookupByCountryFilterRequest(filter: String) object LookupByCountryFilterRequest { - implicit val reads: Reads[LookupByCountryFilterRequest] = Json.reads[LookupByCountryFilterRequest] - implicit val writes: Writes[LookupByCountryFilterRequest] = Json.writes[LookupByCountryFilterRequest] + implicit val reads: Reads[LookupByCountryFilterRequest] = + Json.reads[LookupByCountryFilterRequest] + implicit val writes: Writes[LookupByCountryFilterRequest] = + Json.writes[LookupByCountryFilterRequest] } case class LookupByCountryRequest(country: String, filter: String) object LookupByCountryRequest { - implicit val reads: Reads[LookupByCountryRequest] = Json.reads[LookupByCountryRequest] - implicit val writes: Writes[LookupByCountryRequest] = Json.writes[LookupByCountryRequest] + implicit val reads: Reads[LookupByCountryRequest] = + Json.reads[LookupByCountryRequest] + implicit val writes: Writes[LookupByCountryRequest] = + Json.writes[LookupByCountryRequest] } final case class UserAgent(unwrap: String) diff --git a/app/model/response.scala b/app/model/response.scala index ed5670b..85ef21b 100644 --- a/app/model/response.scala +++ b/app/model/response.scala @@ -25,11 +25,14 @@ object response { import scala.jdk.CollectionConverters._ case class ErrorMessage(msg: List[String], args: List[String]) object ErrorMessage { - val invalidJson: ErrorMessage = ErrorMessage(msg = List("error.payload.invalid"), args = List()) + val invalidJson: ErrorMessage = + ErrorMessage(msg = List("error.payload.invalid"), args = List()) object Implicits { - implicit val errorMessageReads: Reads[ErrorMessage] = Json.reads[ErrorMessage] - implicit val errorMessageWrites: Writes[ErrorMessage] = Json.writes[ErrorMessage] + implicit val errorMessageReads: Reads[ErrorMessage] = + Json.reads[ErrorMessage] + implicit val errorMessageWrites: Writes[ErrorMessage] = + Json.writes[ErrorMessage] } } case class ErrorResponse(obj: List[ErrorMessage]) @@ -38,8 +41,10 @@ object response { object Implicits { import ErrorMessage.Implicits._ - implicit val errorResponseReads: Reads[ErrorResponse] = Json.reads[ErrorResponse] - implicit val errorResponseWrites: Writes[ErrorResponse] = Json.writes[ErrorResponse] + implicit val errorResponseReads: Reads[ErrorResponse] = + Json.reads[ErrorResponse] + implicit val errorResponseWrites: Writes[ErrorResponse] = + Json.writes[ErrorResponse] } } } diff --git a/app/util/package.scala b/app/util/package.scala index 5e57421..18c4e3a 100644 --- a/app/util/package.scala +++ b/app/util/package.scala @@ -28,7 +28,7 @@ package object util { } implicit class NullableString(s: String) { - def emptyToNull: String = if(s.trim.isEmpty) null else s.trim + def emptyToNull: String = if (s.trim.isEmpty) null else s.trim def emptyToNone: Option[String] = Option(emptyToNull) } } diff --git a/project/plugins.sbt b/project/plugins.sbt index 547fec9..7600853 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,8 +1,14 @@ resolvers += Resolver.typesafeRepo("releases") -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) +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) addSbtPlugin("uk.gov.hmrc" % "sbt-auto-build" % "3.22.0") addSbtPlugin("uk.gov.hmrc" % "sbt-distributables" % "2.5.0") addSbtPlugin("org.playframework" % "sbt-plugin" % "3.0.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.9") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.9") diff --git a/test/controllers/AddressSearchControllerTest.scala b/test/controllers/AddressSearchControllerTest.scala index dbd1597..8cc18ba 100644 --- a/test/controllers/AddressSearchControllerTest.scala +++ b/test/controllers/AddressSearchControllerTest.scala @@ -20,8 +20,16 @@ import audit.Auditor import config.AppConfig import connectors.DownstreamConnector import model.address._ -import model.request.{LookupByPostTownRequest, LookupByPostcodeRequest, LookupByUprnRequest} -import model.{AddressSearchAuditEvent, AddressSearchAuditEventMatchedAddress, AddressSearchAuditEventRequestDetails} +import model.request.{ + LookupByPostTownRequest, + LookupByPostcodeRequest, + LookupByUprnRequest +} +import model.{ + AddressSearchAuditEvent, + AddressSearchAuditEventMatchedAddress, + AddressSearchAuditEventRequestDetails +} import org.apache.pekko.stream.Materializer import org.mockito.ArgumentMatchers.{any, eq => meq} import org.mockito.Mockito._ @@ -43,37 +51,105 @@ import scala.concurrent.ExecutionContext import scala.concurrent.duration._ import scala.language.postfixOps -class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOneAppPerSuite with MockitoSugar { +class AddressSearchControllerTest + extends AnyWordSpec + with Matchers + with GuiceOneAppPerSuite + with MockitoSugar { - implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global + implicit val ec: ExecutionContext = + scala.concurrent.ExecutionContext.Implicits.global implicit val timeout: FiniteDuration = 1 second - private val cc: ControllerComponents = play.api.test.Helpers.stubControllerComponents() + private val cc: ControllerComponents = + play.api.test.Helpers.stubControllerComponents() private val en = "en" import model.address.Country._ - val fx1A = AddressRecord("GB100002", Some(100002L), Some(10000200L), Some(1000020L), Some("gb-oragnisation-2"), Address(List("1 Test Street"), "Testtown", "FZ22 7ZW", Some(England), GB), en, Some(LocalCustodian(9999, "Somewhere")), Some(Location("0,0").toSeq), Some("TestLocalAuthority")) - val fx1B = AddressRecord("GB100003", Some(100003L), Some(10000300L), Some(1000030L), Some("gb-oragnisation-3"), Address(List("2 Test Street"), "Testtown", "FZ22 7ZW", Some(England), GB), en, Some(LocalCustodian(9999, "Somewhere")), Some(Location("0,0").toSeq), Some("TestLocalAuthority")) - val fx1C = AddressRecord("GB100004", Some(100004L), Some(10000400L), Some(1000040L), Some("gb-oragnisation-4"), Address(List("3 Test Street"), "Testtown", "FZ22 7ZW", Some(England), GB), en, Some(LocalCustodian(9999, "Somewhere")), Some(Location("0,0").toSeq), Some("TestLocalAuthority")) - - val addressAr1 = AddressRecord("GB100005", Some(100005L), Some(10000500L), Some(1000050L), Some("gb-oragnisation-5"), Address(List("Test Road"), "ATown", "FX11 7LX", Some(England), GB), en, Some(LocalCustodian(2935, "Testland")), Some(Location("12.345678", "-12.345678").toSeq), Some("TestLocalAuthority")) - val addressAr2 = AddressRecord("GB100006", Some(100006L), Some(10000600L), Some(1000060L), Some("gb-oragnisation-6"), Address(List("Test Station", "Test Road"), "ATown", "FX11 7LA", Some(England), GB), en, Some(LocalCustodian(2935, "Testland")), Some(Location("12.345678", "-12.345678").toSeq), Some("TestLocalAuthority")) + val fx1A = AddressRecord( + "GB100002", + Some(100002L), + Some(10000200L), + Some(1000020L), + Some("gb-oragnisation-2"), + Address(List("1 Test Street"), "Testtown", "FZ22 7ZW", Some(England), GB), + en, + Some(LocalCustodian(9999, "Somewhere")), + Some(Location("0,0").toSeq), + Some("TestLocalAuthority") + ) + val fx1B = AddressRecord( + "GB100003", + Some(100003L), + Some(10000300L), + Some(1000030L), + Some("gb-oragnisation-3"), + Address(List("2 Test Street"), "Testtown", "FZ22 7ZW", Some(England), GB), + en, + Some(LocalCustodian(9999, "Somewhere")), + Some(Location("0,0").toSeq), + Some("TestLocalAuthority") + ) + val fx1C = AddressRecord( + "GB100004", + Some(100004L), + Some(10000400L), + Some(1000040L), + Some("gb-oragnisation-4"), + Address(List("3 Test Street"), "Testtown", "FZ22 7ZW", Some(England), GB), + en, + Some(LocalCustodian(9999, "Somewhere")), + Some(Location("0,0").toSeq), + Some("TestLocalAuthority") + ) + + val addressAr1 = AddressRecord( + "GB100005", + Some(100005L), + Some(10000500L), + Some(1000050L), + Some("gb-oragnisation-5"), + Address(List("Test Road"), "ATown", "FX11 7LX", Some(England), GB), + en, + Some(LocalCustodian(2935, "Testland")), + Some(Location("12.345678", "-12.345678").toSeq), + Some("TestLocalAuthority") + ) + val addressAr2 = AddressRecord( + "GB100006", + Some(100006L), + Some(10000600L), + Some(1000060L), + Some("gb-oragnisation-6"), + Address( + List("Test Station", "Test Road"), + "ATown", + "FX11 7LA", + Some(England), + GB + ), + en, + Some(LocalCustodian(2935, "Testland")), + Some(Location("12.345678", "-12.345678").toSeq), + Some("TestLocalAuthority") + ) val mockAuditConnector = mock[AuditConnector] - override implicit lazy val app: Application = { new GuiceApplicationBuilder() .overrides(inject.bind[AuditConnector].toInstance(mockAuditConnector)) .configure( "microservice.services.access-control.enabled" -> true, "microservice.services.access-control.allow-list.1" -> "test-user-agent", - "microservice.services.access-control.allow-list.2" -> "another-user-agent") + "microservice.services.access-control.allow-list.2" -> "another-user-agent" + ) .build() } - val controller: AddressSearchController = app.injector.instanceOf[AddressSearchController] + val controller: AddressSearchController = + app.injector.instanceOf[AddressSearchController] implicit val mat: Materializer = app.injector.instanceOf[Materializer] "findPostTown" should { @@ -83,18 +159,24 @@ class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOn """ in { clearInvocations(mockAuditConnector) - val payload = LookupByPostTownRequest("Testtown", Some("Test Street")) val request = FakeRequest("POST", "/lookup/by-post-town") .withBody(payload) - .withHeaders(HeaderNames.USER_AGENT -> "forbidden-user-agent", HeaderNames.CONTENT_TYPE -> MimeTypes.JSON) + .withHeaders( + HeaderNames.USER_AGENT -> "forbidden-user-agent", + HeaderNames.CONTENT_TYPE -> MimeTypes.JSON + ) val result = controller.searchByPostTown().apply(request) contentType(result) shouldBe Some(MimeTypes.JSON) - contentAsJson(result) shouldBe JsObject(Map( - "code" -> JsNumber(FORBIDDEN), - "description" -> JsString("One or more user agents in 'forbidden-user-agent' are not authorized to use this service. Please complete 'https://forms.office.com/Pages/ResponsePage.aspx?id=PPdSrBr9mkqOekokjzE54cRTj_GCzpRJqsT4amG0JK1UMkpBS1NUVDhWR041NjJWU0lCMVZUNk5NTi4u' to request access.") - )) + contentAsJson(result) shouldBe JsObject( + Map( + "code" -> JsNumber(FORBIDDEN), + "description" -> JsString( + "One or more user agents in 'forbidden-user-agent' are not authorized to use this service. Please complete 'https://forms.office.com/Pages/ResponsePage.aspx?id=PPdSrBr9mkqOekokjzE54cRTj_GCzpRJqsT4amG0JK1UMkpBS1NUVDhWR041NjJWU0lCMVZUNk5NTi4u' to request access." + ) + ) + ) status(result) shouldBe Status.FORBIDDEN } @@ -105,27 +187,106 @@ class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOn """ in { clearInvocations(mockAuditConnector) - val payload = LookupByPostTownRequest("town", Some("address lines")) val request = FakeRequest("POST", "/lookup/by-post-town") .withBody(payload) - .withHeaders(HeaderNames.USER_AGENT -> "test-user-agent", HeaderNames.CONTENT_TYPE -> MimeTypes.JSON) + .withHeaders( + HeaderNames.USER_AGENT -> "test-user-agent", + HeaderNames.CONTENT_TYPE -> MimeTypes.JSON + ) .withHeadersOrigin - val expectedAuditRequestDetails = AddressSearchAuditEventRequestDetails(postTown = Some("TOWN"), filter = Some("address lines")) + val expectedAuditRequestDetails = AddressSearchAuditEventRequestDetails( + postTown = Some("TOWN"), + filter = Some("address lines") + ) val expectedAuditAddressMatches = Seq( - AddressSearchAuditEventMatchedAddress("990091234568",None,None,None,List("Address with 2 Address Lines", "Second Address Line"),"Town",Some(LocalCustodian(425,"WYCOMBE")),None,None,None,"ZZ1Z 6AB",Some(Country("GB-ENG","England")),Country("GB","United Kingdom")), - AddressSearchAuditEventMatchedAddress("990091234637",None,None,None,List("Address with 2 Address Lines", "Second Address Line"),"Town",Some(LocalCustodian(6810,"GWYNEDD")),None,None,None,"FX52 9SJ",Some(Country("GB-ENG","England")),Country("GB","United Kingdom")), - AddressSearchAuditEventMatchedAddress("990091234569",None,None,None,List("Address with 3 Address Lines", "Second Address Line", "Third Address Line"),"Town",Some(LocalCustodian(425,"WYCOMBE")),None,None,None,"ZZ1Z 7AB",Some(Country("GB-ENG","England")),Country("GB","United Kingdom")), - AddressSearchAuditEventMatchedAddress("990091234638",None,None,None,List("Address with 3 Address Lines", "Second Address Line", "Third Address Line"),"Town",Some(LocalCustodian(6810,"GWYNEDD")),None,None,None,"FX0 2GJ",Some(Country("GB-ENG","England")),Country("GB","United Kingdom"))) - val expectedAuditEvent = AddressSearchAuditEvent(Some("test-user-agent"), expectedAuditRequestDetails, 4, expectedAuditAddressMatches) + AddressSearchAuditEventMatchedAddress( + "990091234568", + None, + None, + None, + List("Address with 2 Address Lines", "Second Address Line"), + "Town", + Some(LocalCustodian(425, "WYCOMBE")), + None, + None, + None, + "ZZ1Z 6AB", + Some(Country("GB-ENG", "England")), + Country("GB", "United Kingdom") + ), + AddressSearchAuditEventMatchedAddress( + "990091234637", + None, + None, + None, + List("Address with 2 Address Lines", "Second Address Line"), + "Town", + Some(LocalCustodian(6810, "GWYNEDD")), + None, + None, + None, + "FX52 9SJ", + Some(Country("GB-ENG", "England")), + Country("GB", "United Kingdom") + ), + AddressSearchAuditEventMatchedAddress( + "990091234569", + None, + None, + None, + List( + "Address with 3 Address Lines", + "Second Address Line", + "Third Address Line" + ), + "Town", + Some(LocalCustodian(425, "WYCOMBE")), + None, + None, + None, + "ZZ1Z 7AB", + Some(Country("GB-ENG", "England")), + Country("GB", "United Kingdom") + ), + AddressSearchAuditEventMatchedAddress( + "990091234638", + None, + None, + None, + List( + "Address with 3 Address Lines", + "Second Address Line", + "Third Address Line" + ), + "Town", + Some(LocalCustodian(6810, "GWYNEDD")), + None, + None, + None, + "FX0 2GJ", + Some(Country("GB-ENG", "England")), + Country("GB", "United Kingdom") + ) + ) + val expectedAuditEvent = AddressSearchAuditEvent( + Some("test-user-agent"), + expectedAuditRequestDetails, + 4, + expectedAuditAddressMatches + ) val result = controller.searchByPostTown().apply(request) status(result) shouldBe Status.OK verify(mockAuditConnector, times(1)) - .sendExplicitAudit(meq("AddressSearch"), meq(expectedAuditEvent))(any(), any(), any()) + .sendExplicitAudit(meq("AddressSearch"), meq(expectedAuditEvent))( + any(), + any(), + any() + ) } """when search is called with a posttown that gives no results @@ -133,17 +294,22 @@ class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOn """ in { clearInvocations(mockAuditConnector) - val payload = LookupByPostTownRequest("non-existent-town", None) val request = FakeRequest("POST", "/lookup/by-post-town") .withBody(payload) - .withHeaders(HeaderNames.USER_AGENT -> "test-user-agent", HeaderNames.CONTENT_TYPE -> MimeTypes.JSON) + .withHeaders( + HeaderNames.USER_AGENT -> "test-user-agent", + HeaderNames.CONTENT_TYPE -> MimeTypes.JSON + ) .withHeadersOrigin val result = controller.searchByPostTown().apply(request) status(result) shouldBe Status.OK - verify(mockAuditConnector, never()).sendExplicitAudit(any(), any[AddressSearchAuditEvent]())(any(), any(), any()) + verify(mockAuditConnector, never()).sendExplicitAudit( + any(), + any[AddressSearchAuditEvent]() + )(any(), any(), any()) } } @@ -156,7 +322,10 @@ class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOn val payload = LookupByPostcodeRequest(Postcode("FX11 4HG")) val request = FakeRequest("POST", "/lookup") .withBody(payload) - .withHeaders(HeaderNames.USER_AGENT -> "forbidden-user-agent", HeaderNames.CONTENT_TYPE -> MimeTypes.JSON) + .withHeaders( + HeaderNames.USER_AGENT -> "forbidden-user-agent", + HeaderNames.CONTENT_TYPE -> MimeTypes.JSON + ) val result = controller.searchByPostcode().apply(request) status(result) shouldBe Status.FORBIDDEN @@ -172,7 +341,10 @@ class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOn val payload = LookupByPostcodeRequest(Postcode("FX11 4HG"), Some("FOO")) val request = FakeRequest("POST", "/lookup") .withBody(payload) - .withHeaders(HeaderNames.USER_AGENT -> "test-user-agent", HeaderNames.CONTENT_TYPE -> MimeTypes.JSON) + .withHeaders( + HeaderNames.USER_AGENT -> "test-user-agent", + HeaderNames.CONTENT_TYPE -> MimeTypes.JSON + ) .withHeadersOrigin val result = controller.searchByPostcode().apply(request) @@ -188,7 +360,10 @@ class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOn val payload = LookupByPostcodeRequest(Postcode("FX11 4HG"), None) val request = FakeRequest("POST", "/lookup") .withBody(payload) - .withHeaders(HeaderNames.USER_AGENT -> "test-user-agent", HeaderNames.CONTENT_TYPE -> MimeTypes.JSON) + .withHeaders( + HeaderNames.USER_AGENT -> "test-user-agent", + HeaderNames.CONTENT_TYPE -> MimeTypes.JSON + ) .withHeadersOrigin val result = controller.searchByPostcode().apply(request) @@ -202,34 +377,191 @@ class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOn """ in { clearInvocations(mockAuditConnector) - val payload = LookupByPostcodeRequest(Postcode("ZZ11 1ZZ"), Some("Test Street")) + val payload = + LookupByPostcodeRequest(Postcode("ZZ11 1ZZ"), Some("Test Street")) val request = FakeRequest("POST", "/lookup") .withBody(payload) - .withHeaders(HeaderNames.USER_AGENT -> "test-user-agent", HeaderNames.CONTENT_TYPE -> MimeTypes.JSON) + .withHeaders( + HeaderNames.USER_AGENT -> "test-user-agent", + HeaderNames.CONTENT_TYPE -> MimeTypes.JSON + ) .withHeadersOrigin - val expectedAuditRequestDetails = AddressSearchAuditEventRequestDetails(postcode = Some("ZZ11 1ZZ"), uprn = None, filter = Some("Test Street")) + val expectedAuditRequestDetails = AddressSearchAuditEventRequestDetails( + postcode = Some("ZZ11 1ZZ"), + uprn = None, + filter = Some("Test Street") + ) val expectedAuditAddressMatches = Seq( - AddressSearchAuditEventMatchedAddress("990091234512", None, None, None, List("10 Test Street"), "Testtown", Some(LocalCustodian(121, "NORTH SOMERSET")), None, None, None, "ZZ11 1ZZ", Some(Country("GB-ENG", "England")), Country("GB", "United Kingdom")), - AddressSearchAuditEventMatchedAddress("990091234513", None, None, None, List("11 Test Street"), "Testtown", Some(LocalCustodian(121, "NORTH SOMERSET")), None, None, None, "ZZ11 1ZZ", Some(Country("GB-ENG", "England")), Country("GB", "United Kingdom")), - AddressSearchAuditEventMatchedAddress("990091234504", None, None, None, List("4 Test Street"), "Testtown", Some(LocalCustodian(121, "NORTH SOMERSET")), None, None, None, "ZZ11 1ZZ", Some(Country("GB-ENG", "England")), Country("GB", "United Kingdom")), - AddressSearchAuditEventMatchedAddress("990091234505", None, None, None, List("5 Test Street"), "Testtown", Some(LocalCustodian(121, "NORTH SOMERSET")), None, None, None, "ZZ11 1ZZ", Some(Country("GB-ENG", "England")), Country("GB", "United Kingdom")), - AddressSearchAuditEventMatchedAddress("990091234506", None, None, None, List("6 Test Street"), "Testtown", Some(LocalCustodian(121, "NORTH SOMERSET")), None, None, None, "ZZ11 1ZZ", Some(Country("GB-ENG", "England")), Country("GB", "United Kingdom")), - AddressSearchAuditEventMatchedAddress("990091234510", None, None, None, List("8 Test Street"), "Testtown", Some(LocalCustodian(121, "NORTH SOMERSET")), None, None, None, "ZZ11 1ZZ", Some(Country("GB-ENG", "England")), Country("GB", "United Kingdom")), - AddressSearchAuditEventMatchedAddress("990091234511", None, None, None, List("9 Test Street"), "Testtown", Some(LocalCustodian(121, "NORTH SOMERSET")), None, None, None, "ZZ11 1ZZ", Some(Country("GB-ENG", "England")), Country("GB", "United Kingdom")), - AddressSearchAuditEventMatchedAddress("990091234507", None, None, None, List("Flat 1a", "7 Test Street"), "Testtown", Some(LocalCustodian(121, "NORTH SOMERSET")), None, None, None, "ZZ11 1ZZ", Some(Country("GB-ENG", "England")), Country("GB", "United Kingdom")), - AddressSearchAuditEventMatchedAddress("990091234508", None, None, None, List("Flat 1b", "7 Test Street"), "Testtown", Some(LocalCustodian(121, "NORTH SOMERSET")), None, None, None, "ZZ11 1ZZ", Some(Country("GB-ENG", "England")), Country("GB", "United Kingdom")), - AddressSearchAuditEventMatchedAddress("990091234509", None, None, None, List("Flat 2a", "7 Test Street"), "Testtown", Some(LocalCustodian(121, "NORTH SOMERSET")), None, None, None, "ZZ11 1ZZ", Some(Country("GB-ENG", "England")), Country("GB", "United Kingdom")) + AddressSearchAuditEventMatchedAddress( + "990091234512", + None, + None, + None, + List("10 Test Street"), + "Testtown", + Some(LocalCustodian(121, "NORTH SOMERSET")), + None, + None, + None, + "ZZ11 1ZZ", + Some(Country("GB-ENG", "England")), + Country("GB", "United Kingdom") + ), + AddressSearchAuditEventMatchedAddress( + "990091234513", + None, + None, + None, + List("11 Test Street"), + "Testtown", + Some(LocalCustodian(121, "NORTH SOMERSET")), + None, + None, + None, + "ZZ11 1ZZ", + Some(Country("GB-ENG", "England")), + Country("GB", "United Kingdom") + ), + AddressSearchAuditEventMatchedAddress( + "990091234504", + None, + None, + None, + List("4 Test Street"), + "Testtown", + Some(LocalCustodian(121, "NORTH SOMERSET")), + None, + None, + None, + "ZZ11 1ZZ", + Some(Country("GB-ENG", "England")), + Country("GB", "United Kingdom") + ), + AddressSearchAuditEventMatchedAddress( + "990091234505", + None, + None, + None, + List("5 Test Street"), + "Testtown", + Some(LocalCustodian(121, "NORTH SOMERSET")), + None, + None, + None, + "ZZ11 1ZZ", + Some(Country("GB-ENG", "England")), + Country("GB", "United Kingdom") + ), + AddressSearchAuditEventMatchedAddress( + "990091234506", + None, + None, + None, + List("6 Test Street"), + "Testtown", + Some(LocalCustodian(121, "NORTH SOMERSET")), + None, + None, + None, + "ZZ11 1ZZ", + Some(Country("GB-ENG", "England")), + Country("GB", "United Kingdom") + ), + AddressSearchAuditEventMatchedAddress( + "990091234510", + None, + None, + None, + List("8 Test Street"), + "Testtown", + Some(LocalCustodian(121, "NORTH SOMERSET")), + None, + None, + None, + "ZZ11 1ZZ", + Some(Country("GB-ENG", "England")), + Country("GB", "United Kingdom") + ), + AddressSearchAuditEventMatchedAddress( + "990091234511", + None, + None, + None, + List("9 Test Street"), + "Testtown", + Some(LocalCustodian(121, "NORTH SOMERSET")), + None, + None, + None, + "ZZ11 1ZZ", + Some(Country("GB-ENG", "England")), + Country("GB", "United Kingdom") + ), + AddressSearchAuditEventMatchedAddress( + "990091234507", + None, + None, + None, + List("Flat 1a", "7 Test Street"), + "Testtown", + Some(LocalCustodian(121, "NORTH SOMERSET")), + None, + None, + None, + "ZZ11 1ZZ", + Some(Country("GB-ENG", "England")), + Country("GB", "United Kingdom") + ), + AddressSearchAuditEventMatchedAddress( + "990091234508", + None, + None, + None, + List("Flat 1b", "7 Test Street"), + "Testtown", + Some(LocalCustodian(121, "NORTH SOMERSET")), + None, + None, + None, + "ZZ11 1ZZ", + Some(Country("GB-ENG", "England")), + Country("GB", "United Kingdom") + ), + AddressSearchAuditEventMatchedAddress( + "990091234509", + None, + None, + None, + List("Flat 2a", "7 Test Street"), + "Testtown", + Some(LocalCustodian(121, "NORTH SOMERSET")), + None, + None, + None, + "ZZ11 1ZZ", + Some(Country("GB-ENG", "England")), + Country("GB", "United Kingdom") + ) ) - val expectedAuditEvent = AddressSearchAuditEvent(Some("test-user-agent"), expectedAuditRequestDetails, 10, expectedAuditAddressMatches) + val expectedAuditEvent = AddressSearchAuditEvent( + Some("test-user-agent"), + expectedAuditRequestDetails, + 10, + expectedAuditAddressMatches + ) val result = controller.searchByPostcode().apply(request) status(result) shouldBe Status.OK verify(mockAuditConnector, times(1)) - .sendExplicitAudit(meq("AddressSearch"), meq(expectedAuditEvent))(any(), any(), any()) + .sendExplicitAudit(meq("AddressSearch"), meq(expectedAuditEvent))( + any(), + any(), + any() + ) } """when search is called with a postcode that gives no results @@ -240,24 +572,32 @@ class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOn val payload = LookupByPostcodeRequest(Postcode("ZZ11 1YY")) val request = FakeRequest("POST", "/lookup") .withBody(payload) - .withHeaders(HeaderNames.USER_AGENT -> "test-user-agent", HeaderNames.CONTENT_TYPE -> MimeTypes.JSON) + .withHeaders( + HeaderNames.USER_AGENT -> "test-user-agent", + HeaderNames.CONTENT_TYPE -> MimeTypes.JSON + ) .withHeadersOrigin val result = controller.searchByPostcode().apply(request) status(result) shouldBe Status.OK - verify(mockAuditConnector, never()).sendExplicitAudit(any(), any[AddressSearchAuditEvent]())(any(), any(), any()) + verify(mockAuditConnector, never()).sendExplicitAudit( + any(), + any[AddressSearchAuditEvent]() + )(any(), any(), any()) } } - "uprn lookup with POST request" should { "give forbidden" when { """search is called without a valid user agent""" in { val payload = LookupByUprnRequest("0123456789") val request = FakeRequest("POST", "/lookup/by-uprn") .withBody(payload) - .withHeaders(HeaderNames.USER_AGENT -> "forbidden-user-agent", HeaderNames.CONTENT_TYPE -> MimeTypes.JSON) + .withHeaders( + HeaderNames.USER_AGENT -> "forbidden-user-agent", + HeaderNames.CONTENT_TYPE -> MimeTypes.JSON + ) .withHeadersOrigin val response = controller.searchByUprn().apply(request) @@ -270,25 +610,52 @@ class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOn clearInvocations(mockAuditConnector) - val expectedAuditRequestDetails = AddressSearchAuditEventRequestDetails(uprn = Some("790091234501")) + val expectedAuditRequestDetails = + AddressSearchAuditEventRequestDetails(uprn = Some("790091234501")) val expectedAuditAddressMatches = Seq( - AddressSearchAuditEventMatchedAddress("790091234501",None,None,None,List("1 Test Street"),"Testtown",Some(LocalCustodian(9010,"SHETLAND ISLANDS")),None,None,None,"BB00 0BB",Some(Country("GB-SCT","Scotland")),Country("GB","United Kingdom")) + AddressSearchAuditEventMatchedAddress( + "790091234501", + None, + None, + None, + List("1 Test Street"), + "Testtown", + Some(LocalCustodian(9010, "SHETLAND ISLANDS")), + None, + None, + None, + "BB00 0BB", + Some(Country("GB-SCT", "Scotland")), + Country("GB", "United Kingdom") + ) ) - val expectedAuditEvent = AddressSearchAuditEvent(Some("test-user-agent"), expectedAuditRequestDetails, 10, expectedAuditAddressMatches) + val expectedAuditEvent = AddressSearchAuditEvent( + Some("test-user-agent"), + expectedAuditRequestDetails, + 10, + expectedAuditAddressMatches + ) val payload = LookupByUprnRequest("790091234501") val request = FakeRequest("POST", "/lookup/by-uprn") .withBody(payload) - .withHeaders(HeaderNames.USER_AGENT -> "test-user-agent", HeaderNames.CONTENT_TYPE -> MimeTypes.JSON) + .withHeaders( + HeaderNames.USER_AGENT -> "test-user-agent", + HeaderNames.CONTENT_TYPE -> MimeTypes.JSON + ) .withHeadersOrigin val response = controller.searchByUprn().apply(request) status(response) shouldBe 200 verify(mockAuditConnector, never()) - .sendExplicitAudit(any(), meq(expectedAuditEvent))(any(), any(), any()) + .sendExplicitAudit(any(), meq(expectedAuditEvent))( + any(), + any(), + any() + ) } } @@ -297,11 +664,15 @@ class AddressSearchControllerTest extends AnyWordSpec with Matchers with GuiceOn val connector = app.injector.instanceOf[DownstreamConnector] val configHelper = app.injector.instanceOf[AppConfig] val auditor = app.injector.instanceOf[Auditor] - val controller = new AddressSearchController(connector, auditor, cc, configHelper)(ec) + val controller = + new AddressSearchController(connector, auditor, cc, configHelper)(ec) val payload = LookupByUprnRequest("GB0123456789") val request = FakeRequest("POST", "/lookup/by-uprn") .withBody(payload) - .withHeaders(HeaderNames.USER_AGENT -> "test-user-agent", HeaderNames.CONTENT_TYPE -> MimeTypes.JSON) + .withHeaders( + HeaderNames.USER_AGENT -> "test-user-agent", + HeaderNames.CONTENT_TYPE -> MimeTypes.JSON + ) .withHeadersOrigin val response = controller.searchByUprn().apply(request) diff --git a/test/model/AddressTest.scala b/test/model/AddressTest.scala index 3789e80..4260ba2 100644 --- a/test/model/AddressTest.scala +++ b/test/model/AddressTest.scala @@ -23,43 +23,52 @@ import org.scalatest.matchers.should.Matchers class AddressTest extends AnyFunSuite with Matchers { import model.address.Country._ - test( - """Given an address with only one line and a town + test("""Given an address with only one line and a town then 'printable' with newline should generate the correct string""") { val a = Address(List("ATown"), "some-town", "FX1 1XX", Some(England), GB) a.printable("\n") shouldBe "ATown\nsome-town\nFX1 1XX" } - test( - """An address with only one line and a town is valid""") { + test("""An address with only one line and a town is valid""") { val a = Address(List("ATown"), "some-town", "FX1 1XX", Some(England), GB) a.isValid shouldBe true } - test( - """Given an address with three lines, and a town, + test("""Given an address with three lines, and a town, then 'printable' should generate the correct string""") { - val a = Address(List("Line1", "Line2", "Line3"), "ATown", "FX1 1XX", Some(Wales), GB) + val a = Address( + List("Line1", "Line2", "Line3"), + "ATown", + "FX1 1XX", + Some(Wales), + GB + ) a.printable shouldBe "Line1, Line2, Line3, ATown, FX1 1XX" } - test( - """Given an address with three lines and a town, + test("""Given an address with three lines and a town, when a truncated address is generated, then all of the lines should be no more than 35 characters, and the town should be no more than 35 characters and trailing whitespace should be removed""") { - val a = Address(List( - "This is Line1 and is very long so long that it is more than 35 chars", - "This is Line2 and is very long so long that it is more than 35 chars", - "This is Line3 and is very long so long that it is more than 35 chars"), - "Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch", "FX1 1XX", Some(England), GB).truncatedAddress() + val a = Address( + List( + "This is Line1 and is very long so long that it is more than 35 chars", + "This is Line2 and is very long so long that it is more than 35 chars", + "This is Line3 and is very long so long that it is more than 35 chars" + ), + "Llanfairpwllgwyngyllgogerychwyrndrobwllllantysiliogogogoch", + "FX1 1XX", + Some(England), + GB + ).truncatedAddress() val expected = List( - //23456789-123456789-123456789-12345 + // 23456789-123456789-123456789-12345 "This is Line1 and is very long so l", "This is Line2 and is very long so l", - "This is Line3 and is very long so l") + "This is Line3 and is very long so l" + ) for (i <- 0 until 3) { a.lines(i).length <= 35 shouldBe true @@ -68,43 +77,75 @@ class AddressTest extends AnyFunSuite with Matchers { a.town.length shouldBe 35 } - test( - """An address with three lines and a town is valid""") { - val a = Address(List("Line1", "Line2", "Line3"), "ATown", "FX1 1XX", Some(Wales), GB) + test("""An address with three lines and a town is valid""") { + val a = Address( + List("Line1", "Line2", "Line3"), + "ATown", + "FX1 1XX", + Some(Wales), + GB + ) a.isValid shouldBe true } - test( - """An address with no lines and a town is not valid""") { + test("""An address with no lines and a town is not valid""") { val a = Address(Nil, "ATown", "FX1 1XX", Some(Wales), GB) !a.isValid shouldBe true } - test( - """An address with four lines and a town is not valid""") { - val a = Address(List("a", "b", "c", "d"), "ATown", "FX1 1XX", Some(Wales), GB) + test("""An address with four lines and a town is not valid""") { + val a = + Address(List("a", "b", "c", "d"), "ATown", "FX1 1XX", Some(Wales), GB) !a.isValid shouldBe true } - test( - """An address with five lines is not valid""") { - val a = Address(List("a", "b", "c", "d", "e"), "some-town", "FX1 1XX", Some(Wales), GB) + test("""An address with five lines is not valid""") { + val a = Address( + List("a", "b", "c", "d", "e"), + "some-town", + "FX1 1XX", + Some(Wales), + GB + ) !a.isValid shouldBe true } - test( - """Given a valid address in a record with a two-letter language, + test("""Given a valid address in a record with a two-letter language, then the record should be valid""") { - val a = Address(List("Line1", "Line2", "Line3"), "ATown", "FX1 1XX", Some(Wales), GB) - val ar = address.AddressRecord("abc123", None, None, None, None, a, "en", None, None) + val a = Address( + List("Line1", "Line2", "Line3"), + "ATown", + "FX1 1XX", + Some(Wales), + GB + ) + val ar = address.AddressRecord( + "abc123", + None, + None, + None, + None, + a, + "en", + None, + None + ) ar.isValid shouldBe true } test( """Given a valid address in a record that does not have a two-letter language, - then the record should be invalid""") { - val a = Address(List("Line1", "Line2", "Line3"), "ATown", "FX1 1XX", Some(Wales), GB) - val ar = address.AddressRecord("abc123", None, None, None, None, a, "", None, None) + then the record should be invalid""" + ) { + val a = Address( + List("Line1", "Line2", "Line3"), + "ATown", + "FX1 1XX", + Some(Wales), + GB + ) + val ar = + address.AddressRecord("abc123", None, None, None, None, a, "", None, None) !ar.isValid shouldBe true } } diff --git a/test/model/PostcodeTest.scala b/test/model/PostcodeTest.scala index 0e73172..e3bdd48 100644 --- a/test/model/PostcodeTest.scala +++ b/test/model/PostcodeTest.scala @@ -21,24 +21,21 @@ import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers class PostcodeTest extends AnyFunSuite with Matchers { - test( - """Given valid postcode string should cleanup successfully""") { + test("""Given valid postcode string should cleanup successfully""") { val pc = "FX11XX" val maybePostcode = Postcode.cleanupPostcode(pc) maybePostcode shouldBe defined maybePostcode.get.toString shouldBe "FX1 1XX" } - test( - """Given valid short postcode string should cleanup successfully""") { + test("""Given valid short postcode string should cleanup successfully""") { val pc = "W12DN" val maybePostcode = Postcode.cleanupPostcode(pc) maybePostcode shouldBe defined maybePostcode.get.toString shouldBe "W1 2DN" } - test( - """Given valid long postcode string should cleanup successfully""") { + test("""Given valid long postcode string should cleanup successfully""") { val pc = "DE128HJ" val maybePostcode = Postcode.cleanupPostcode(pc) maybePostcode shouldBe defined @@ -46,7 +43,8 @@ class PostcodeTest extends AnyFunSuite with Matchers { } test( - """Given valid edge case postcode string should cleanup successfully""") { + """Given valid edge case postcode string should cleanup successfully""" + ) { val pc = "GIR0AA" val maybePostcode = Postcode.cleanupPostcode(pc) maybePostcode shouldBe defined diff --git a/test/model/RequestTest.scala b/test/model/RequestTest.scala index fd51147..aa625a2 100644 --- a/test/model/RequestTest.scala +++ b/test/model/RequestTest.scala @@ -28,8 +28,10 @@ class RequestTest extends AnyWordSpec with Matchers { "LookupByPostTownRequest" should { "de-serialise filter correctly" when { "filter is specified and is non-empty" in { - val postTownJson = Json.parse("""{"posttown":"WINDSOR", "filter":"some-filter"}""") - val postTownRequest = Json.fromJson[LookupByPostTownRequest](postTownJson) + val postTownJson = + Json.parse("""{"posttown":"WINDSOR", "filter":"some-filter"}""") + val postTownRequest = + Json.fromJson[LookupByPostTownRequest](postTownJson) postTownRequest shouldBe a[JsSuccess[_]] postTownRequest.get.posttown shouldBe "WINDSOR" postTownRequest.get.filter shouldBe Some("some-filter") @@ -37,7 +39,8 @@ class RequestTest extends AnyWordSpec with Matchers { "filter is not specified" in { val postTownJson = Json.parse("""{"posttown":"WINDSOR"}""") - val postTownRequest = Json.fromJson[LookupByPostTownRequest](postTownJson) + val postTownRequest = + Json.fromJson[LookupByPostTownRequest](postTownJson) postTownRequest shouldBe a[JsSuccess[_]] postTownRequest.get.posttown shouldBe "WINDSOR" postTownRequest.get.filter shouldBe None @@ -45,7 +48,8 @@ class RequestTest extends AnyWordSpec with Matchers { "filter is specified but is empty" in { val postTownJson = Json.parse("""{"posttown":"WINDSOR", "filter":""}""") - val postTownRequest = Json.fromJson[LookupByPostTownRequest](postTownJson) + val postTownRequest = + Json.fromJson[LookupByPostTownRequest](postTownJson) postTownRequest shouldBe a[JsSuccess[_]] postTownRequest.get.posttown shouldBe "WINDSOR" postTownRequest.get.filter shouldBe None @@ -58,8 +62,10 @@ class RequestTest extends AnyWordSpec with Matchers { "de-serialise correctly" when { "filter is specified and is non-empty" in { - val postCodeJson = Json.parse("""{"postcode":"SW6 6SA", "filter":"some-filter"}""") - val postCodeRequest = Json.fromJson[LookupByPostcodeRequest](postCodeJson) + val postCodeJson = + Json.parse("""{"postcode":"SW6 6SA", "filter":"some-filter"}""") + val postCodeRequest = + Json.fromJson[LookupByPostcodeRequest](postCodeJson) postCodeRequest shouldBe a[JsSuccess[_]] postCodeRequest.get.postcode shouldBe Postcode("SW6 6SA") postCodeRequest.get.filter shouldBe Some("some-filter") @@ -67,7 +73,8 @@ class RequestTest extends AnyWordSpec with Matchers { "filter is not specified" in { val postCodeJson = Json.parse("""{"postcode":"SW6 6SA"}""") - val postCodeRequest = Json.fromJson[LookupByPostcodeRequest](postCodeJson) + val postCodeRequest = + Json.fromJson[LookupByPostcodeRequest](postCodeJson) postCodeRequest shouldBe a[JsSuccess[_]] postCodeRequest.get.postcode shouldBe Postcode("SW6 6SA") postCodeRequest.get.filter shouldBe None @@ -75,7 +82,8 @@ class RequestTest extends AnyWordSpec with Matchers { "filter is specified but is empty" in { val postCodeJson = Json.parse("""{"postcode":"SW6 6SA", "filter":""}""") - val postCodeRequest = Json.fromJson[LookupByPostcodeRequest](postCodeJson) + val postCodeRequest = + Json.fromJson[LookupByPostcodeRequest](postCodeJson) postCodeRequest shouldBe a[JsSuccess[_]] postCodeRequest.get.postcode shouldBe Postcode("SW6 6SA") postCodeRequest.get.filter shouldBe None diff --git a/test/util/utils.scala b/test/util/utils.scala index a4eeb7f..176fe4e 100644 --- a/test/util/utils.scala +++ b/test/util/utils.scala @@ -22,9 +22,9 @@ import play.api.test.FakeRequest import java.lang.annotation._ - object Utils { - lazy val headerOrigin: String = ConfigFactory.load().getString("header.x-origin") + lazy val headerOrigin: String = + ConfigFactory.load().getString("header.x-origin") implicit class FakeRequestWithOrigin[T](fake: FakeRequest[T]) { def withHeadersOrigin: FakeRequest[T] =