Skip to content

Commit

Permalink
APIS-4691 - Add Url validation rule (#68)
Browse files Browse the repository at this point in the history
* APIS-4691 - Initial commit

* APIS-4691 - Add url validation rule and rework tests to utilize

* APIS-4691 - Fix case object use

* APIS-4691 - Apply review comment
  • Loading branch information
AndySpaven authored Apr 16, 2020
1 parent 47fc4f9 commit 658231a
Show file tree
Hide file tree
Showing 11 changed files with 115 additions and 46 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ trait AcceptanceTestSpec extends FeatureSpec

protected def fieldsIdEndpoint(fieldsId: UUID) = s"/field/$fieldsId"

protected val SampleFields1 = Map(fieldN(1) -> "value1", fieldN(2) -> "value2")
protected val SampleFields2 = Map(fieldN(1) -> "value1b", fieldN(3) -> "value3")
protected val SampleFields1 = Map(fieldN(1) -> "http://www.example.com/some-endpoint", fieldN(2) -> "value2")
protected val SampleFields2 = Map(fieldN(1) -> "https://www.example2.com/updated", fieldN(3) -> "value3")

protected def validSubscriptionPutRequest(fields: Fields): FakeRequest[AnyContentAsJson] =
validSubscriptionPutRequest(SubscriptionFieldsRequest(fields))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class ApiSubscriptionFieldsHappySpec extends AcceptanceTestSpec
val putRequest = validDefinitionPutRequest(FieldsDefinitionRequest(FakeFieldsDefinitions))
.withTarget( RequestTarget(uriString="", path=definitionEndpoint(fakeRawContext, fakeRawVersion), queryString = Map.empty))

val r = Await.result(route(app, putRequest).get, 10.seconds)
Await.result(route(app, putRequest).get, 10.seconds)
}

override def afterAll() {
Expand All @@ -67,9 +67,8 @@ class ApiSubscriptionFieldsHappySpec extends AcceptanceTestSpec
result shouldBe 'defined
val resultFuture = result.value

val r = Await.result(resultFuture, 10.seconds)

status(resultFuture) shouldBe CREATED

And("the response body should be a valid response")
val sfr = contentAsJson(resultFuture).validate[SubscriptionFieldsResponse]
val fieldsId = sfr.get.fieldsId
Expand Down Expand Up @@ -189,6 +188,7 @@ class ApiSubscriptionFieldsHappySpec extends AcceptanceTestSpec
result shouldBe 'defined
val resultFuture = result.value


status(resultFuture) shouldBe OK

And("the response body should be a valid response")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ trait NonEmptyListFormatters {
}

trait JsonFormatters extends SharedJsonFormatters with NonEmptyListFormatters {
import be.venneborg.refined.play.RefinedJsonFormats._

implicit val validationRuleFormat: OFormat[ValidationRule] = derived.withTypeTag.oformat(TypeTagSetting.ShortClassName)

Expand Down
17 changes: 15 additions & 2 deletions app/uk/gov/hmrc/apisubscriptionfields/model/Model.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,28 @@ case class ApiVersion(value: String) extends AnyVal

case class SubscriptionFieldsId(value: UUID) extends AnyVal

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

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

case class RegexValidationRule(regex: String) extends ValidationRule
case object UrlValidationRule extends ValidationRule {
import eu.timepit.refined.string._
import eu.timepit.refined._

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

case class ValidationGroup(errorMessage: String, rules: NEL[ValidationRule])

object FieldDefinitionType extends Enumeration {
type FieldDefinitionType = Value

// TODO - complete "since" when release is ready
@deprecated("We don't use URL type for any validation", since = "0.5x")
val URL = Value("URL")
val SECURE_TOKEN = Value("SecureToken")
val STRING = Value("STRING")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class SubscriptionFieldsService @Inject() (repository: SubscriptionFieldsReposit

fieldDefinitions.map(
_.fold[SubsFieldValidationResponse](throw new RuntimeException)(fieldDefinitions =>
SubscriptionFieldsService.validate(fieldDefinitions, fields) ++ SubscriptionFieldsService.validateFieldNamesAreDefined(fieldDefinitions,fields) match {
SubscriptionFieldsService.validateAgainstValidationRules(fieldDefinitions, fields) ++ SubscriptionFieldsService.validateFieldNamesAreDefined(fieldDefinitions,fields) match {
case FieldErrorMap.empty => ValidSubsFieldValidationResponse
case errs: FieldErrorMap =>
InvalidSubsFieldValidationResponse(errorResponses = errs)
Expand Down Expand Up @@ -111,24 +111,19 @@ object SubscriptionFieldsService {
val empty = Map.empty[FieldName, ErrorMessage]
}

// True - passed
def validateAgainstRule(rule: ValidationRule, value: String): Boolean = rule match {
case RegexValidationRule(regex) => value.matches(regex)
}

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

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

def validate(fieldDefinitions: NonEmptyList[FieldDefinition], fields: Fields): FieldErrorMap =
def validateAgainstValidationRules(fieldDefinitions: NonEmptyList[FieldDefinition], fields: Fields): FieldErrorMap =
fieldDefinitions
.map(fd => validateAgainstDefinition(fd, fields.get(fd.name).getOrElse("")))
.map(fd => validateAgainstDefinition(fd, fields.getOrElse(fd.name,"")))
.foldLeft(FieldErrorMap.empty) {
case (acc, None) => acc
case (acc, Some((name,msg))) => acc + (name -> msg)
Expand Down
5 changes: 4 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ val compile = Seq(
"uk.gov.hmrc" %% "simple-reactivemongo" % "7.22.0-play-26",
"org.julienrf" %% "play-json-derived-codecs" % "6.0.0",
"com.typesafe.play" %% "play-json" % "2.7.1",
"org.typelevel" %% "cats-core" % "2.0.0"
"org.typelevel" %% "cats-core" % "2.1.0",
"eu.timepit" %% "refined" % "0.9.13",
// "eu.timepit" %% "refined-cats" % "0.9.13"
"be.venneborg" %% "play26-refined" % "0.5.0"
)

// we need to override the akka version for now as newer versions are not compatible with reactivemongo
Expand Down
5 changes: 3 additions & 2 deletions test/uk/gov/hmrc/apisubscriptionfields/TestData.scala
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ trait TestData {
def fieldN(id: Int): String = s"field_$id"
}

trait SubscriptionFieldsTestData extends TestData {
trait SubscriptionFieldsTestData extends TestData with ValidationRuleTestData {

final val FakeClientId = ClientId(fakeRawClientId)
final val FakeClientId2 = ClientId(fakeRawClientId2)
Expand Down Expand Up @@ -86,7 +86,8 @@ trait SubscriptionFieldsTestData extends TestData {
trait FieldsDefinitionTestData extends TestData {
val FakeValidationRule: RegexValidationRule = RegexValidationRule(".*")
val FakeValidation: ValidationGroup = ValidationGroup("error message", NonEmptyList.one(FakeValidationRule))
final val FakeFieldDefinitionUrl = FieldDefinition(fieldN(1), "desc1", "hint1", FieldDefinitionType.URL, "short description", Some(FakeValidation))
val FakeUrlValidation: ValidationGroup = ValidationGroup("error message", NonEmptyList.one(UrlValidationRule))
final val FakeFieldDefinitionUrl = FieldDefinition(fieldN(1), "desc1", "hint1", FieldDefinitionType.URL, "short description", Some(FakeUrlValidation))
final val FakeFieldDefinitionUrlValidationEmpty = FieldDefinition(fieldN(1), "desc1", "hint1", FieldDefinitionType.URL, "short description", None)
final val FakeFieldDefinitionString = FieldDefinition(fieldN(2), "desc2", "hint2", FieldDefinitionType.STRING, "short description", Some(FakeValidation))
final val FakeFieldDefinitionSecureToken = FieldDefinition(fieldN(3), "desc3", "hint3", FieldDefinitionType.SECURE_TOKEN, "short description", Some(FakeValidation))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class JsonFormatterSpec extends WordSpec with Matchers with JsonFormatters with
private val subscriptionFieldJson =
s"""{"clientId":"$fakeRawClientId","apiContext":"$fakeRawContext","apiVersion":"$fakeRawVersion","fieldsId":"$FakeRawFieldsId","fields":{"f1":"v1"}}"""
private val fieldDefinitionJson =
s"""{"apiContext":"$fakeRawContext","apiVersion":"$fakeRawVersion","fieldDefinitions":[{"name":"${fieldN(1)}","description":"desc1","hint":"hint1","type":"URL","shortDescription":"short description","validation":{"errorMessage":"error message","rules":[{"RegexValidationRule":{"regex":".*"}}]}}]}"""
s"""{"apiContext":"$fakeRawContext","apiVersion":"$fakeRawVersion","fieldDefinitions":[{"name":"${fieldN(1)}","description":"desc1","hint":"hint1","type":"URL","shortDescription":"short description","validation":{"errorMessage":"error message","rules":[{"UrlValidationRule":{}}]}}]}"""
private val fieldDefinitionEmptyValidationJson =
s"""{"apiContext":"$fakeRawContext","apiVersion":"$fakeRawVersion","fieldDefinitions":[{"name":"${fieldN(1)}","description":"desc1","hint":"hint1","type":"URL","shortDescription":"short description"}]}"""

Expand Down
49 changes: 49 additions & 0 deletions test/uk/gov/hmrc/apisubscriptionfields/model/ModelSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* 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.model

import uk.gov.hmrc.play.test.UnitSpec
import uk.gov.hmrc.apisubscriptionfields.SubscriptionFieldsTestData
import uk.gov.hmrc.apisubscriptionfields.FieldsDefinitionTestData


class ModelSpec extends UnitSpec with SubscriptionFieldsTestData with FieldsDefinitionTestData with ValidationRuleTestData {
"RegexValidationRule" should {

"return true when the value is valid - correct case" in {
lowerCaseRule.validate(lowerCaseValue) shouldBe true
}
"return true when the value is valid - long enough" in {
atLeastThreeLongRule.validate(lowerCaseValue) shouldBe true
}
"return false when the value is invalid - wrong case" in {
lowerCaseRule.validate(mixedCaseValue) shouldBe false
}
"return false when the value is invalid - too short" in {
atLeastTenLongRule.validate(mixedCaseValue) shouldBe false
}
}

"UrlValidationRule" should {
"pass for a matching value" in {
UrlValidationRule.validate(validUrl) shouldBe true
}
"fail for a value that does not match" in {
UrlValidationRule.validate(invalidUrl) shouldBe false
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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.model

trait ValidationRuleTestData {

val lowerCaseValue = "bob"
val mixedCaseValue = "Bob"

val lowerCaseRule: ValidationRule = RegexValidationRule("""^[a-z]+$""")
val mixedCaseRule: ValidationRule = RegexValidationRule("""^[a-zA-Z]+$""")

val atLeastThreeLongRule: ValidationRule = RegexValidationRule("""^.{3}.*$""")
val atLeastTenLongRule: ValidationRule = RegexValidationRule("""^.{10}.*$""")

val validUrl = "https://www.example.com/here/and/there"
val invalidUrl = "www.example.com"
}
Original file line number Diff line number Diff line change
Expand Up @@ -200,34 +200,9 @@ class SubscriptionFieldsServiceSpec extends UnitSpec with SubscriptionFieldsTest
}
}

val lowerCaseValue = "bob"
val mixedCaseValue = "Bob"

val lowerCaseRule: ValidationRule = RegexValidationRule("""^[a-z]+$""")
val mixedCaseRule: ValidationRule = RegexValidationRule("""^[a-zA-Z]+$""")

val atLeastThreeLongRule: ValidationRule = RegexValidationRule("""^.{3}.*$""")
val atLeastTenLongRule: ValidationRule = RegexValidationRule("""^.{10}.*$""")

def theErrorMessage(i: Int) = s"error message $i"
val validationGroup1: ValidationGroup = ValidationGroup(theErrorMessage(1), NonEmptyList(mixedCaseRule, List(atLeastThreeLongRule)))

"validate value against rule" should {

"return true when the value is valid - correct case" in {
SubscriptionFieldsService.validateAgainstRule(lowerCaseRule, lowerCaseValue) shouldBe true
}
"return true when the value is valid - long enough" in {
SubscriptionFieldsService.validateAgainstRule(atLeastThreeLongRule, lowerCaseValue) shouldBe true
}
"return false when the value is invalid - wrong case" in {
SubscriptionFieldsService.validateAgainstRule(lowerCaseRule, mixedCaseValue) shouldBe false
}
"return false when the value is invalid - too short" in {
SubscriptionFieldsService.validateAgainstRule(atLeastTenLongRule, mixedCaseValue) shouldBe false
}
}

"validate value against group" should {

"return true when the value is both mixed case and at least 3 long" in {
Expand Down

0 comments on commit 658231a

Please sign in to comment.