Skip to content

Commit

Permalink
Merge pull request #71 from hmrc/APIS-4691-Validate
Browse files Browse the repository at this point in the history
APIS-4691 - extend validate functionality
  • Loading branch information
AndySpaven authored Apr 20, 2020
2 parents 3d82d4f + c37446d commit c70fc7c
Show file tree
Hide file tree
Showing 18 changed files with 266 additions and 174 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ class SubscriptionFieldDefinitionsUnhappySpec extends AcceptanceTestSpec
status(resultFuture) shouldBe NOT_FOUND

And("the response body contains error message")
contentAsJson(resultFuture) shouldBe JsErrorResponse(NOT_FOUND_CODE, s"Fields definition not found for ($fakeRawContext, unknown)")
contentAsJson(resultFuture) shouldBe JsErrorResponse(NOT_FOUND_CODE, s"Fields definition not found for (${FakeContext.value}, unknown)")
}

scenario("the API is called to PUT a fields definition with an invalid JSON payload") {
Expand Down
51 changes: 51 additions & 0 deletions app/uk/gov/hmrc/apisubscriptionfields/controller/Binders.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright 2020 HM Revenue & Customs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package uk.gov.hmrc.apisubscriptionfields.controller

import play.api.mvc.PathBindable
import uk.gov.hmrc.apisubscriptionfields.model.{ApiContext,ApiVersion, ClientId, SubscriptionFieldsId}
import java.util.UUID

object Binders {

implicit object apiContextPathBindable extends PathBindable.Parsing[ApiContext](
ApiContext.apply,
_.value,
(key: String, e: Exception) => "Cannot parse parameter %s as ApiContext: %s".format(key, e.getMessage)
)

implicit object apiVersionPathBindable extends PathBindable.Parsing[ApiVersion](
ApiVersion.apply,
_.value,
(key: String, e: Exception) => "Cannot parse parameter %s as ApiVersion: %s".format(key, e.getMessage)
)

implicit object clientIdPathBindable extends PathBindable.Parsing[ClientId](
ClientId.apply,
_.value,
(key: String, e: Exception) => "Cannot parse parameter %s as ClientId: %s".format(key, e.getMessage)
)

implicit object subscriptionFieldsIdPathBindable extends PathBindable.Parsing[SubscriptionFieldsId](
(s) => SubscriptionFieldsId(UUID.fromString(s)),
_.value.toString,
(key: String, e: Exception) => "Cannot parse parameter %s as SubscriptionFieldsId: %s".format(key, e.getMessage)
)
}



Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,46 @@ package uk.gov.hmrc.apisubscriptionfields.controller

import javax.inject.{Inject, Singleton}

import play.api.libs.json.{JsValue, Json}
import play.api.libs.json.{JsValue, Json, JsSuccess, JsError}
import play.api.mvc._
import uk.gov.hmrc.apisubscriptionfields.model._
import uk.gov.hmrc.apisubscriptionfields.service.FieldsDefinitionService

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.{Try,Success,Failure}
import play.api.Logger
import java.util.UUID

@Singleton
class FieldsDefinitionController @Inject() (cc: ControllerComponents, service: FieldsDefinitionService) extends CommonController {

import JsonFormatters._

private def notFoundResponse(rawApiContext: String, rawApiVersion: String) =
NotFound(JsErrorResponse(ErrorCode.NOT_FOUND_CODE, s"Fields definition not found for ($rawApiContext, $rawApiVersion)"))
private def badRequestWithTag(fn: (UUID) => String): Result = {
val errorTag = java.util.UUID.randomUUID()
Logger.warn(fn(errorTag))
BadRequest(s"""{"tag": "$errorTag"}""")
}

private def notFoundResponse(apiContext: ApiContext, apiVersion: ApiVersion) =
NotFound(JsErrorResponse(ErrorCode.NOT_FOUND_CODE, s"Fields definition not found for (${apiContext.value}, ${apiVersion.value})"))

def validateFieldsDefinition(): Action[JsValue] = Action(parse.json) { request =>
Try(request.body.validate[FieldsDefinitionRequest]) match {
case Success(JsSuccess(payload, _)) => Ok("")
case Success(JsError(errs)) => {
badRequestWithTag( (tag:UUID) => s"A JSON error occurred: [${tag.toString}] ${Json.prettyPrint(JsError.toJson(errs))}")
}
case Failure(e) => {
badRequestWithTag{ (tag:UUID) => s"An error occurred during JSON validation: [${tag.toString}] ${e.getMessage}" }
}
}
}

def upsertFieldsDefinition(rawApiContext: String, rawApiVersion: String): Action[JsValue] = Action.async(parse.json) { implicit request =>
def upsertFieldsDefinition(apiContext: ApiContext, apiVersion: ApiVersion): Action[JsValue] = Action.async(parse.json) { implicit request =>
withJsonBody[FieldsDefinitionRequest] { payload =>
service.upsert(ApiContext(rawApiContext), ApiVersion(rawApiVersion), payload.fieldDefinitions) map {
service.upsert(apiContext, apiVersion, payload.fieldDefinitions) map {
case (response, true) => Created(Json.toJson(response))
case (response, false) => Ok(Json.toJson(response))
}
Expand All @@ -47,22 +68,22 @@ class FieldsDefinitionController @Inject() (cc: ControllerComponents, service: F
service.getAll map (defs => Ok(Json.toJson(defs))) recover recovery
}

def getFieldsDefinition(rawApiContext: String, rawApiVersion: String): Action[AnyContent] = Action.async { _ =>
val eventualMaybeResponse = service.get(ApiContext(rawApiContext), ApiVersion(rawApiVersion))
asActionResult(eventualMaybeResponse, rawApiContext, rawApiVersion)
def getFieldsDefinition(apiContext: ApiContext, apiVersion: ApiVersion): Action[AnyContent] = Action.async { _ =>
val eventualMaybeResponse = service.get(apiContext, apiVersion)
asActionResult(eventualMaybeResponse, apiContext, apiVersion)
}

def deleteFieldsDefinition(rawApiContext: String, rawApiVersion: String): Action[AnyContent] = Action.async { _ =>
service.delete(ApiContext(rawApiContext), ApiVersion(rawApiVersion)) map {
def deleteFieldsDefinition(apiContext: ApiContext, apiVersion: ApiVersion): Action[AnyContent] = Action.async { _ =>
service.delete(apiContext, apiVersion) map {
case true => NoContent
case false => notFoundResponse(rawApiContext, rawApiVersion)
case false => notFoundResponse(apiContext, apiVersion)
} recover recovery
}

private def asActionResult(eventualMaybeResponse: Future[Option[FieldsDefinitionResponse]], rawApiContext: String, rawApiVersion: String) = {
private def asActionResult(eventualMaybeResponse: Future[Option[FieldsDefinitionResponse]], apiContext: ApiContext, apiVersion: ApiVersion) = {
eventualMaybeResponse map {
case Some(subscriptionFields) => Ok(Json.toJson(subscriptionFields))
case None => notFoundResponse(rawApiContext, rawApiVersion)
case None => notFoundResponse(apiContext, apiVersion)
} recover recovery
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@

package uk.gov.hmrc.apisubscriptionfields.controller

import java.util.UUID

import javax.inject.{Inject, Singleton}
import play.api.libs.json._
import play.api.mvc._
Expand All @@ -37,31 +35,31 @@ class SubscriptionFieldsController @Inject()(cc: ControllerComponents, service:
NotFound(JsErrorResponse(NOT_FOUND_CODE, message))
}

private def notFoundMessage(rawClientId: String, rawApiContext: String, rawApiVersion: String): String = {
s"Subscription fields not found for ($rawClientId, $rawApiContext, $rawApiVersion)"
private def notFoundMessage(clientId: ClientId, apiContext: ApiContext, apiVersion: ApiVersion): String = {
s"Subscription fields not found for (${clientId.value}, ${apiContext.value}, ${apiVersion.value})"
}

private def notFoundMessage(rawFieldsId: UUID): String = {
s"FieldsId (${rawFieldsId.toString}) was not found"
private def notFoundMessage(subscriptionFieldsId: SubscriptionFieldsId): String = {
s"FieldsId (${subscriptionFieldsId.value.toString}) was not found"
}

private def notFoundMessage(rawClientId: String): String = {
s"ClientId ($rawClientId) was not found"
private def notFoundMessage(clientId: ClientId): String = {
s"ClientId (${clientId.value}) was not found"
}

def getSubscriptionFields(rawClientId: String, rawApiContext: String, rawApiVersion: String): Action[AnyContent] = Action.async { _ =>
val eventualMaybeResponse = service.get(ClientId(rawClientId), ApiContext(rawApiContext), ApiVersion(rawApiVersion))
asActionResult(eventualMaybeResponse, notFoundMessage(rawClientId, rawApiContext, rawApiVersion))
def getSubscriptionFields(clientId: ClientId, apiContext: ApiContext, apiVersion: ApiVersion): Action[AnyContent] = Action.async { _ =>
val eventualMaybeResponse = service.get(clientId, apiContext, apiVersion)
asActionResult(eventualMaybeResponse, notFoundMessage(clientId, apiContext, apiVersion))
}

def getSubscriptionFieldsByFieldsId(rawFieldsId: UUID): Action[AnyContent] = Action.async { _ =>
val eventualMaybeResponse = service.get(SubscriptionFieldsId(rawFieldsId))
asActionResult(eventualMaybeResponse, notFoundMessage(rawFieldsId))
def getSubscriptionFieldsByFieldsId(subscriptionFieldsId: SubscriptionFieldsId): Action[AnyContent] = Action.async { _ =>
val eventualMaybeResponse = service.get(subscriptionFieldsId)
asActionResult(eventualMaybeResponse, notFoundMessage(subscriptionFieldsId))
}

def getBulkSubscriptionFieldsByClientId(rawClientId: String): Action[AnyContent] = Action.async { _ =>
val eventualMaybeResponse = service.get(ClientId(rawClientId))
asBulkActionResult(eventualMaybeResponse, notFoundMessage(rawClientId))
def getBulkSubscriptionFieldsByClientId(clientId: ClientId): Action[AnyContent] = Action.async { _ =>
val eventualMaybeResponse = service.get(clientId)
asBulkActionResult(eventualMaybeResponse, notFoundMessage(clientId))
}

def getAllSubscriptionFields: Action[AnyContent] = Action.async { _ =>
Expand All @@ -82,17 +80,17 @@ class SubscriptionFieldsController @Inject()(cc: ControllerComponents, service:
} recover recovery
}

def upsertSubscriptionFields(rawClientId: String, rawApiContext: String, rawApiVersion: String): Action[JsValue] = Action.async(parse.json) { implicit request =>
def upsertSubscriptionFields(clientId: ClientId, apiContext: ApiContext, apiVersion: ApiVersion): Action[JsValue] = Action.async(parse.json) { implicit request =>
import JsonFormatters._

withJsonBody[SubscriptionFieldsRequest] { payload =>
if(payload.fields.isEmpty) {
Future.successful(UnprocessableEntity(JsErrorResponse(INVALID_REQUEST_PAYLOAD, "At least one field must be specified")))
}
else {
service.validate(ApiContext(rawApiContext), ApiVersion(rawApiVersion), payload.fields) flatMap {
service.validate(apiContext, apiVersion, payload.fields) flatMap {
case ValidSubsFieldValidationResponse => {
service.upsert(ClientId(rawClientId), ApiContext(rawApiContext), ApiVersion(rawApiVersion), payload.fields) map {
service.upsert(clientId, apiContext, apiVersion, payload.fields) map {
case (response, true) => Created(Json.toJson(response))
case (response, false) => Ok(Json.toJson(response))
}
Expand All @@ -103,17 +101,17 @@ class SubscriptionFieldsController @Inject()(cc: ControllerComponents, service:
}
}

def deleteSubscriptionFields(rawClientId: String, rawApiContext: String, rawApiVersion: String): Action[AnyContent] = Action.async { _ =>
service.delete(ClientId(rawClientId), ApiContext(rawApiContext), ApiVersion(rawApiVersion)) map {
def deleteSubscriptionFields(clientId: ClientId, apiContext: ApiContext, apiVersion: ApiVersion): Action[AnyContent] = Action.async { _ =>
service.delete(clientId, apiContext, apiVersion) map {
case true => NoContent
case false => notFoundResponse(notFoundMessage(rawClientId, rawApiContext, rawApiVersion))
case false => notFoundResponse(notFoundMessage(clientId, apiContext, apiVersion))
} recover recovery
}

def deleteAllSubscriptionFieldsForClient(rawClientId: String): Action[AnyContent] = Action.async { _ =>
service.delete(ClientId(rawClientId)) map {
def deleteAllSubscriptionFieldsForClient(clientId: ClientId): Action[AnyContent] = Action.async { _ =>
service.delete(clientId) map {
case true => NoContent
case false => notFoundResponse(notFoundMessage(rawClientId))
case false => notFoundResponse(notFoundMessage(clientId))
} recover recovery
}

Expand Down
20 changes: 5 additions & 15 deletions app/uk/gov/hmrc/apisubscriptionfields/model/JsonFormatters.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,16 @@

package uk.gov.hmrc.apisubscriptionfields.model

import java.util.UUID

import cats.data.{NonEmptyList => NEL}
import julienrf.json.derived
import play.api.libs.json._
import julienrf.json.derived.TypeTagSetting.ShortClassName
import play.api.libs.json.Reads._
import play.api.libs.functional.syntax._
import julienrf.json.derived.TypeTagSetting.ShortClassName
import Types._
import uk.gov.hmrc.apisubscriptionfields.model.FieldDefinitionType.FieldDefinitionType
import Types._

trait SharedJsonFormatters {
implicit val SubscriptionFieldsIdJF = new Format[SubscriptionFieldsId] {
def writes(s: SubscriptionFieldsId) = JsString(s.value.toString)

def reads(json: JsValue) = json match {
case JsNull => JsError()
case _ => JsSuccess(SubscriptionFieldsId(json.as[UUID]))
}
}
}

trait NonEmptyListFormatters {

implicit def nelReads[A](implicit r: Reads[A]): Reads[NEL[A]] =
Expand All @@ -55,7 +43,9 @@ trait NonEmptyListFormatters {
.contramap(_.toList)
}

trait JsonFormatters extends SharedJsonFormatters with NonEmptyListFormatters {
trait JsonFormatters extends NonEmptyListFormatters {
implicit val SubscriptionFieldsIdjsonFormat = Json.valueFormat[SubscriptionFieldsId]

import be.venneborg.refined.play.RefinedJsonFormats._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
Expand Down
7 changes: 3 additions & 4 deletions app/uk/gov/hmrc/apisubscriptionfields/model/Model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,15 @@ case class ApiVersion(value: String) extends AnyVal
case class SubscriptionFieldsId(value: UUID) extends AnyVal

sealed trait ValidationRule {
def validate(value: String): Boolean
def validate(value: FieldValue): Boolean
}

case class RegexValidationRule(regex: RegexExpr) extends ValidationRule {
def validate(value: String): Boolean = value.matches(regex.value)
def validate(value: FieldValue): Boolean = value.matches(regex.value)
}

case object UrlValidationRule extends ValidationRule {

def validate(value: String): Boolean = refineV[NonFtpUrl](value).isRight
def validate(value: FieldValue): Boolean = refineV[NonFtpUrl](value).isRight
}

case class ValidationGroup(errorMessage: String, rules: NEL[ValidationRule])
Expand Down
6 changes: 1 addition & 5 deletions app/uk/gov/hmrc/apisubscriptionfields/model/Types.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,21 +20,17 @@ import eu.timepit.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.string._
import eu.timepit.refined.boolean._
import eu.timepit.refined.api.RefType.applyRef

object Types {
type RegexExpr = String Refined Regex
type NonFtpUrl = Url And Not[StartsWith[W.`"ftp"`.T]]

type FieldNameRegex = MatchesRegex[W.`"^[a-zA-Z]+$"`.T]
type FieldName = Refined[String,FieldNameRegex]
object FieldName {
def unsafeApply(s: String): FieldName = Refined.unsafeApply(s)
}

type FieldValue = String

type Fields = Map[FieldName, String]
type Fields = Map[FieldName, FieldValue]

type ErrorMessage = String

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,12 @@ object SubscriptionFieldsService {
import Types._

// True - passed
def validateAgainstGroup(group: ValidationGroup, value: String): Boolean = {
def validateAgainstGroup(group: ValidationGroup, value: FieldValue): Boolean = {
group.rules.foldLeft(true)((acc, rule) => (acc && rule.validate(value)))
}

// Some is Some(error)
def validateAgainstDefinition(fieldDefinition: FieldDefinition, value: String): Option[FieldError] = {
def validateAgainstDefinition(fieldDefinition: FieldDefinition, value: FieldValue): Option[FieldError] = {
fieldDefinition.validation .flatMap(group => if (validateAgainstGroup(group, value)) None else Some((fieldDefinition.name, group.errorMessage)))
}

Expand Down
8 changes: 7 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,12 @@ lazy val microservice = Project(appName, file("."))
.settings(
inConfig(AcceptanceTest)(BloopDefaults.configSettings)
)
.settings(
routesImport ++= Seq(
"uk.gov.hmrc.apisubscriptionfields.model._",
"uk.gov.hmrc.apisubscriptionfields.controller.Binders._"
)
)
.settings(
libraryDependencies ++= appDependencies,
dependencyOverrides ++= overrides,
Expand All @@ -107,7 +113,7 @@ lazy val acceptanceTestSettings =

lazy val scoverageSettings: Seq[Setting[_]] = Seq(
coverageExcludedPackages := "<empty>;Reverse.*;.*model.*;.*config.*;.*(AuthService|BuildInfo|Routes).*;.*.application;.*.definition",
coverageMinimum := 97,
coverageMinimum := 95,
coverageFailOnMinimum := true,
coverageHighlighting := true,
parallelExecution in Test := false
Expand Down
Loading

0 comments on commit c70fc7c

Please sign in to comment.