diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 191db20..5583cca 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,4 +17,4 @@ jobs: message: ":warning: Secrets found in repository ${{ inputs.repo-name }}" slack-url: ${{ secrets.SLACK_WEBHOOK }} - name: Run tests - run: sbt test + run: sbt scalafmtCheckAll test diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..f8aa32c --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,4 @@ +version = 3.7.11 +preset = default +runner.dialect = scala213 +maxColumn = 180 diff --git a/build.sbt b/build.sbt index 3a69ad4..58fabbf 100644 --- a/build.sbt +++ b/build.sbt @@ -56,6 +56,7 @@ lazy val root = (project in file(".")) .settings( name := "tdr-metadata-validation", libraryDependencies ++= Seq( + commonsLang3, scalaTest % Test, ) ) diff --git a/project/Dependencies.scala b/project/Dependencies.scala index d122012..baa8562 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -1,6 +1,6 @@ import sbt._ object Dependencies { + lazy val commonsLang3 = "org.apache.commons" % "commons-lang3" % "3.13.0" lazy val scalaTest = "org.scalatest" %% "scalatest" % "3.2.12" - lazy val mockito = "org.mockito" %% "mockito-scala" % "1.17.7" } diff --git a/project/plugins.sbt b/project/plugins.sbt index 20f8a40..5cfd3e2 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,5 @@ addSbtPlugin("com.github.sbt" % "sbt-release" % "1.1.0") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") resolvers += Resolver.jcenterRepo addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.21") addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1") diff --git a/src/main/scala/uk/gov/nationalarchives/tdr/validation/DataType.scala b/src/main/scala/uk/gov/nationalarchives/tdr/validation/DataType.scala new file mode 100644 index 0000000..74a16f9 --- /dev/null +++ b/src/main/scala/uk/gov/nationalarchives/tdr/validation/DataType.scala @@ -0,0 +1,157 @@ +package uk.gov.nationalarchives.tdr.validation + +import uk.gov.nationalarchives.tdr.validation.ErrorCode._ + +import java.time.{LocalDateTime, Year} +import scala.util.control.Exception.allCatch + +sealed trait DataType extends AnyRef + +case object Integer extends AnyRef with DataType with Product with Serializable { + def checkValue(value: String, criteria: MetadataCriteria): Option[String] = { + value match { + case "" if criteria.required => Some(EMPTY_VALUE_ERROR) + case t if allCatch.opt(t.toInt).isEmpty => Some(NUMBER_ONLY_ERROR) + case t if t.toInt < 0 => Some(NEGATIVE_NUMBER_ERROR) + case _ => None + } + } +} + +case object DateTime extends AnyRef with DataType with Product with Serializable { + def checkValue(value: String, criteria: MetadataCriteria): Option[String] = { + value match { + case "" if criteria.required => Some(EMPTY_VALUE_ERROR) + case "" if !criteria.required => None + case v => + val date = v.replace("T", "-").split("[-:]") + validate(date(2), date(1), date(0), criteria) + } + } + + val isInvalidDay: Int => Boolean = (day: Int) => day < 1 || day > 31 + val isInvalidMonth: Int => Boolean = (month: Int) => month < 1 || month > 12 + val isInvalidYear: Int => Boolean = (year: Int) => year.toString.length != 4 + val isALeapYear: Int => Boolean = (year: Int) => Year.of(year).isLeap + + lazy val monthsWithLessThan31Days: Map[Int, String] = Map( + 2 -> "February", + 4 -> "April", + 6 -> "June", + 9 -> "September", + 11 -> "November" + ) + + private def validate(day: String, month: String, year: String, criteria: MetadataCriteria): Option[String] = { + val emptyDate: Boolean = day.isEmpty && month.isEmpty && year.isEmpty + + emptyDate match { + case false => validateDateValues(day, month, year, criteria) + case true if criteria.required => Some(EMPTY_VALUE_ERROR) + case _ => None + } + } + + private def validateDateValues(day: String, month: String, year: String, criteria: MetadataCriteria): Option[String] = { + val dayError = validateDay(day) + val monthError = if (dayError.isEmpty) validateMonth(month) else dayError + val yearError = if (monthError.isEmpty) validateYear(year) else monthError + val dayForMonthError = if (yearError.isEmpty) checkDayForTheMonthAndYear(day.toInt, month.toInt, year.toInt) else yearError + if (dayForMonthError.isEmpty) checkIfFutureDateIsAllowed(day.toInt, month.toInt, year.toInt, criteria) else dayForMonthError + } + + private def validateDay(day: String): Option[String] = { + day match { + case v if v.isEmpty => Some(EMPTY_VALUE_ERROR_FOR_DAY) + case v if allCatch.opt(v.toInt).isEmpty => Some(NUMBER_ERROR_FOR_DAY) + case v if v.toInt < 0 => Some(NEGATIVE_NUMBER_ERROR_FOR_DAY) + case v if isInvalidDay(v.toInt) => Some(INVALID_NUMBER_ERROR_FOR_DAY) + case _ => None + } + } + + private def validateMonth(month: String): Option[String] = { + month match { + case v if v.isEmpty => Some(EMPTY_VALUE_ERROR_FOR_MONTH) + case v if allCatch.opt(v.toInt).isEmpty => Some(NUMBER_ERROR_FOR_MONTH) + case v if v.toInt < 0 => Some(NEGATIVE_NUMBER_ERROR_FOR_MONTH) + case v if isInvalidMonth(v.toInt) => Some(INVALID_NUMBER_ERROR_FOR_MONTH) + case _ => None + } + } + + private def validateYear(year: String): Option[String] = { + year match { + case v if v.isEmpty => Some(EMPTY_VALUE_ERROR_FOR_YEAR) + case v if allCatch.opt(v.toInt).isEmpty => Some(NUMBER_ERROR_FOR_YEAR) + case v if v.toInt < 0 => Some(NEGATIVE_NUMBER_ERROR_FOR_YEAR) + case v if isInvalidYear(v.toInt) => Some(INVALID_NUMBER_ERROR_FOR_YEAR) + case _ => None + } + } + + private def checkDayForTheMonthAndYear(dayNumber: Int, monthNumber: Int, yearNumber: Int): Option[String] = { + val monthHasLessThan31Days = monthsWithLessThan31Days.contains(monthNumber) + + if (dayNumber > 30 && monthHasLessThan31Days || dayNumber == 30 && monthNumber == 2) { + Some(INVALID_DAY_FOR_MONTH_ERROR) + } else if (dayNumber == 29 && monthNumber == 2 && !isALeapYear(yearNumber)) { + Some(INVALID_DAY_FOR_MONTH_ERROR) + } else { + None + } + } + + private def checkIfFutureDateIsAllowed(day: Int, month: Int, year: Int, criteria: MetadataCriteria): Option[String] = + if (!criteria.isFutureDateAllowed && LocalDateTime.now().isBefore(LocalDateTime.of(year, month, day, 0, 0))) { + Some(FUTURE_DATE_ERROR) + } else { + None + } +} + +case object Text extends AnyRef with DataType with Product with Serializable { + + def checkValue(value: String, criteria: MetadataCriteria): Option[String] = { + val definedValues = criteria.definedValues + value match { + case "" if criteria.required => Some(EMPTY_VALUE_ERROR) + case v if definedValues.nonEmpty && !criteria.isMultiValueAllowed && v.split(",").length > 1 => Some(MULTI_VALUE_ERROR) + case v if definedValues.nonEmpty && !v.split(",").toList.forall(definedValues.contains) => Some(UNDEFINED_VALUE_ERROR) + case _ => None + } + } +} + +case object Boolean extends AnyRef with DataType with Product with Serializable { + def checkValue(value: String, criteria: MetadataCriteria, requiredMetadata: Option[Metadata]): Option[String] = { + value match { + case "" if criteria.required => + if (isRequiredMetadataIsEmpty(criteria, requiredMetadata)) { + None + } else { + Some(NO_OPTION_SELECTED_ERROR) + } + case v if criteria.requiredProperty.isDefined && requiredMetadata.exists(_.value.isEmpty) => Some(REQUIRED_PROPERTY_IS_EMPTY) + case v if !criteria.definedValues.contains(v) => Some(UNDEFINED_VALUE_ERROR) + case _ => None + } + } + + def isRequiredMetadataIsEmpty(criteria: MetadataCriteria, requiredMetadata: Option[Metadata]): Boolean = { + criteria.requiredProperty.isDefined && requiredMetadata.exists(_.value.isEmpty) + } +} +case object Decimal extends AnyRef with DataType with Product with Serializable + +object DataType { + def get(dataType: String): DataType = { + dataType match { + case "Integer" => Integer + case "DateTime" => DateTime + case "Text" => Text + case "Boolean" => Boolean + case "Decimal" => Decimal + } + } +} diff --git a/src/main/scala/uk/gov/nationalarchives/tdr/validation/MetadataCriteria.scala b/src/main/scala/uk/gov/nationalarchives/tdr/validation/MetadataCriteria.scala new file mode 100644 index 0000000..9295985 --- /dev/null +++ b/src/main/scala/uk/gov/nationalarchives/tdr/validation/MetadataCriteria.scala @@ -0,0 +1,47 @@ +package uk.gov.nationalarchives.tdr.validation + +case class Metadata(name: String, value: String) +case class MetadataCriteria( + name: String, + dataType: DataType, + required: Boolean, + isFutureDateAllowed: Boolean, + isMultiValueAllowed: Boolean, + definedValues: List[String], + requiredProperty: Option[String] = None, + dependencies: Option[Map[String, List[MetadataCriteria]]] = None, + defaultValue: Option[String] = None +) + +object MetadataProperty { + val closureType = "ClosureType" + val descriptiveType = "DescriptiveType" +} + +object ErrorCode { + val CLOSURE_STATUS_IS_MISSING = "CLOSURE_STATUS_IS_MISSING" + val CLOSURE_METADATA_EXISTS_WHEN_FILE_IS_OPEN = "CLOSURE_METADATA_EXISTS_WHEN_FILE_IS_OPEN" + val NUMBER_ONLY_ERROR = "NUMBER_ONLY_ERROR" + val NEGATIVE_NUMBER_ERROR = "NEGATIVE_NUMBER_ERROR" + val EMPTY_VALUE_ERROR = "EMPTY_VALUE_ERROR" + val NO_OPTION_SELECTED_ERROR = "NO_OPTION_SELECTED_ERROR" + val INVALID_DATE_FORMAT_ERROR = "INVALID_DATE_FORMAT_ERROR" + val INVALID_NUMBER_ERROR = "INVALID_NUMBER_ERROR" + val EMPTY_VALUE_ERROR_FOR_DAY = "EMPTY_VALUE_ERROR_FOR_DAY" + val NUMBER_ERROR_FOR_DAY = "NUMBER_ERROR_FOR_DAY" + val NEGATIVE_NUMBER_ERROR_FOR_DAY = "NEGATIVE_NUMBER_ERROR_FOR_DAY" + val INVALID_NUMBER_ERROR_FOR_DAY = "INVALID_NUMBER_ERROR_FOR_DAY" + val EMPTY_VALUE_ERROR_FOR_MONTH = "EMPTY_VALUE_ERROR_FOR_MONTH" + val NUMBER_ERROR_FOR_MONTH = "NUMBER_ERROR_FOR_MONTH" + val NEGATIVE_NUMBER_ERROR_FOR_MONTH = "NEGATIVE_NUMBER_ERROR_FOR_MONTH" + val INVALID_NUMBER_ERROR_FOR_MONTH = "INVALID_NUMBER_ERROR_FOR_MONTH" + val EMPTY_VALUE_ERROR_FOR_YEAR = "EMPTY_VALUE_ERROR_FOR_YEAR" + val NUMBER_ERROR_FOR_YEAR = "NUMBER_ERROR_FOR_YEAR" + val NEGATIVE_NUMBER_ERROR_FOR_YEAR = "NEGATIVE_NUMBER_ERROR_FOR_YEAR" + val INVALID_NUMBER_ERROR_FOR_YEAR = "INVALID_NUMBER_ERROR_FOR_YEAR" + val INVALID_DAY_FOR_MONTH_ERROR = "INVALID_DAY_FOR_MONTH_ERROR" + val FUTURE_DATE_ERROR = "FUTURE_DATE_ERROR" + val MULTI_VALUE_ERROR = "MULTI_VALUE_ERROR" + val UNDEFINED_VALUE_ERROR = "UNDEFINED_VALUE_ERROR" + val REQUIRED_PROPERTY_IS_EMPTY = "REQUIRED_PROPERTY_IS_EMPTY" +} diff --git a/src/main/scala/uk/gov/nationalarchives/tdr/validation/MetadataValidation.scala b/src/main/scala/uk/gov/nationalarchives/tdr/validation/MetadataValidation.scala new file mode 100644 index 0000000..fe590b0 --- /dev/null +++ b/src/main/scala/uk/gov/nationalarchives/tdr/validation/MetadataValidation.scala @@ -0,0 +1,71 @@ +package uk.gov.nationalarchives.tdr.validation + +import org.apache.commons.lang3.BooleanUtils +import uk.gov.nationalarchives.tdr.validation.ErrorCode._ +import uk.gov.nationalarchives.tdr.validation.MetadataProperty._ + +case class FileRow(fileName: String, metadata: List[Metadata]) +case class Error(propertyName: String, errorCode: String) + +class MetadataValidation(closureMetadataCriteria: MetadataCriteria, descriptiveMetadataCriteria: List[MetadataCriteria]) { + + def validateMetadata(fileRows: List[FileRow]): Map[String, List[Error]] = { + fileRows.map(row => row.fileName -> (validateClosureMetadata(row.metadata) ++ validateDescriptiveMetadata(row.metadata))).toMap + } + + def validateClosureMetadata(input: List[Metadata]): List[Error] = { + val closureStatus = input.find(_.name == closureType) + closureStatus match { + case Some(Metadata(_, "Open")) => + val hasAnyClosureMetadata = hasClosureMetadata(input, closureMetadataCriteria.dependencies.flatMap(_.get("Closed"))) + if (hasAnyClosureMetadata) { + List(Error(closureType, CLOSURE_METADATA_EXISTS_WHEN_FILE_IS_OPEN)) + } else { + List.empty + } + case Some(Metadata(_, "Closed")) => validateMetadata(input, closureMetadataCriteria.dependencies.flatMap(_.get("Closed")).getOrElse(Nil)) + case Some(Metadata(_, _)) => List(Error(closureType, UNDEFINED_VALUE_ERROR)) + case None => List(Error(closureType, CLOSURE_STATUS_IS_MISSING)) + } + } + + def hasClosureMetadata(input: List[Metadata], metadataCriteria: Option[List[MetadataCriteria]]): Boolean = { + metadataCriteria.exists( + _.exists(criteria => + input.find(_.name == criteria.name).exists(_.value != criteria.defaultValue.getOrElse("")) + || hasClosureMetadata(input, criteria.dependencies.flatMap(_.get(criteria.defaultValue.getOrElse("")))) + ) + ) + } + + def validateDescriptiveMetadata(input: List[Metadata]): List[Error] = validateMetadata(input, descriptiveMetadataCriteria) + + private def validateMetadata(input: List[Metadata], metadataCriteria: List[MetadataCriteria]): List[Error] = { + input.flatMap(metadata => { + metadataCriteria + .find(_.name == metadata.name) + .flatMap(criteria => { + val value = metadata.value + val errorCode = criteria.dataType match { + case Integer | Decimal => Integer.checkValue(value, criteria) + case Boolean => + Boolean.checkValue(value, criteria, criteria.requiredProperty.flatMap(p => input.find(_.name == p))) match { + case None if value.nonEmpty => + criteria.dependencies + .flatMap( + _.collect { + case (definedValue, criteria) if BooleanUtils.toBoolean(definedValue) == BooleanUtils.toBoolean(value) => + validateMetadata(input.filter(r => criteria.exists(_.name == r.name)), criteria.map(_.copy(required = true))).map(_.errorCode).headOption + }.head + ) + case error => error + } + case Text => Text.checkValue(value, criteria) + case DateTime => DateTime.checkValue(value, criteria) + case _ => None + } + errorCode.map(Error(criteria.name, _)) + }) + }) + } +} diff --git a/src/test/scala/uk/gov/nationalarchives/tdr/validation/DataTypeSpec.scala b/src/test/scala/uk/gov/nationalarchives/tdr/validation/DataTypeSpec.scala new file mode 100644 index 0000000..95bb913 --- /dev/null +++ b/src/test/scala/uk/gov/nationalarchives/tdr/validation/DataTypeSpec.scala @@ -0,0 +1,137 @@ +package uk.gov.nationalarchives.tdr.validation + +import org.scalatest.matchers.should.Matchers._ +import org.scalatest.wordspec.AnyWordSpec +import uk.gov.nationalarchives.tdr.validation.ErrorCode._ + +class DataTypeSpec extends AnyWordSpec { + + "Integer" should { + + val criteria = MetadataCriteria("Property1", Integer, true, false, false, Nil, None, None) + + "checkValue should return an error if the value is empty" in { + Integer.checkValue("", criteria) should be(Some(EMPTY_VALUE_ERROR)) + } + + "checkValue should return an error if the value is not an integer" in { + Integer.checkValue("e", criteria) should be(Some(NUMBER_ONLY_ERROR)) + } + + "checkValue should return an error if the value is a negative number" in { + Integer.checkValue("-1", criteria) should be(Some(NEGATIVE_NUMBER_ERROR)) + } + + "checkValue should not return any error if the value is a valid number" in { + Integer.checkValue("1", criteria) should be(None) + } + } + + "DateTime" should { + + val criteria = MetadataCriteria("Property1", DateTime, true, false, false, Nil, None, None) + + "checkValue should not return amy error if the date is valid" in { + DateTime.checkValue("1990-12-10T00:00:00", criteria) should be(None) + DateTime.checkValue("2000-2-29T00:00:00", criteria) should be(None) + } + + "checkValue should not return amy error if the value is empty but it is not mandatory" in { + DateTime.checkValue("", criteria.copy(required = false)) should be(None) + } + + "checkValue should not return amy error if future date is allowed" in { + DateTime.checkValue("1990-12-10T00:00:00", criteria.copy(isFutureDateAllowed = true)) should be(None) + DateTime.checkValue("2090-12-10T00:00:00", criteria.copy(isFutureDateAllowed = true)) should be(None) + } + + "checkValue should return an error if the value is empty" in { + DateTime.checkValue("", criteria) should be(Some(EMPTY_VALUE_ERROR)) + DateTime.checkValue("--T00:00:00", criteria) should be(Some(EMPTY_VALUE_ERROR)) + } + + "checkValue should return an error if the day or month or year is empty" in { + DateTime.checkValue("1990-10-T00:00:00", criteria) should be(Some(EMPTY_VALUE_ERROR_FOR_DAY)) + DateTime.checkValue("1990--10T00:00:00", criteria) should be(Some(EMPTY_VALUE_ERROR_FOR_MONTH)) + DateTime.checkValue("-10-10T00:00:00", criteria) should be(Some(EMPTY_VALUE_ERROR_FOR_YEAR)) + } + + "checkValue should return an error if the day or month or year is not valid" in { + DateTime.checkValue("1990-10-32T00:00:00", criteria) should be(Some(INVALID_NUMBER_ERROR_FOR_DAY)) + DateTime.checkValue("1990-28-10T00:00:00", criteria) should be(Some(INVALID_NUMBER_ERROR_FOR_MONTH)) + DateTime.checkValue("19999-10-10T00:00:00", criteria) should be(Some(INVALID_NUMBER_ERROR_FOR_YEAR)) + DateTime.checkValue("199-10-10T00:00:00", criteria) should be(Some(INVALID_NUMBER_ERROR_FOR_YEAR)) + } + + "checkValue should return an error if the day is not valid for given month" in { + DateTime.checkValue("1990-2-29T00:00:00", criteria) should be(Some(INVALID_DAY_FOR_MONTH_ERROR)) + DateTime.checkValue("1990-4-31T00:00:00", criteria) should be(Some(INVALID_DAY_FOR_MONTH_ERROR)) + DateTime.checkValue("1990-6-31T00:00:00", criteria) should be(Some(INVALID_DAY_FOR_MONTH_ERROR)) + DateTime.checkValue("1990-9-31T00:00:00", criteria) should be(Some(INVALID_DAY_FOR_MONTH_ERROR)) + DateTime.checkValue("1990-11-31T00:00:00", criteria) should be(Some(INVALID_DAY_FOR_MONTH_ERROR)) + } + + "checkValue should return an error if future date is not allowed" in { + DateTime.checkValue("2050-2-1T00:00:00", criteria) should be(Some(FUTURE_DATE_ERROR)) + } + } + + "Text" should { + + val criteria = MetadataCriteria("Property1", Text, true, false, false, Nil, None, None) + + "checkValue should not return any error if the value is valid" in { + Text.checkValue("hello", criteria) should be(None) + } + + "checkValue should not return any error if the value is empty but it is not mandatory" in { + Text.checkValue("", criteria.copy(required = false)) should be(None) + } + + "checkValue should return an error if the value is empty" in { + Text.checkValue("", criteria) should be(Some(EMPTY_VALUE_ERROR)) + } + + "checkValue should not return any error if the value has multiple values but multiple values are allowed" in { + Text.checkValue("22,44", criteria.copy(definedValues = List("22", "44"), isMultiValueAllowed = true)) should be(None) + } + + "checkValue should return an error if the value has multiple value but multiple values are not allowed" in { + Text.checkValue("22,44", criteria.copy(definedValues = List("22", "44"))) should be(Some(MULTI_VALUE_ERROR)) + } + + "checkValue should return an error if the value is not matching with defined values" in { + Text.checkValue("22,44", criteria.copy(definedValues = List("22", "33"), isMultiValueAllowed = true)) should be(Some(UNDEFINED_VALUE_ERROR)) + } + } + + "Boolean" should { + + val criteria = MetadataCriteria("Property1", Boolean, true, false, false, List("yes", "no"), None, None) + + "checkValue should not return any error if the value is valid" in { + Boolean.checkValue("yes", criteria, None) should be(None) + } + + "checkValue should not return any error if the value is empty but required property is empty too" in { + Boolean.checkValue("", criteria.copy(requiredProperty = Some("property2")), Some(Metadata("property2", ""))) should be(None) + } + + "checkValue should return an error if the value is empty and no required property" in { + Boolean.checkValue("", criteria, None) should be(Some(NO_OPTION_SELECTED_ERROR)) + } + + "checkValue should return an error if the value is empty and has required property" in { + Boolean.checkValue("", criteria.copy(requiredProperty = Some("property2")), Some(Metadata("property2", "value"))) should be(Some(NO_OPTION_SELECTED_ERROR)) + } + + "checkValue should return an error if the value is not matching with the defined values" in { + Boolean.checkValue("true", criteria, None) should be(Some(UNDEFINED_VALUE_ERROR)) + } + + "checkValue should return an error if the required property value is empty" in { + Boolean.checkValue("yes", criteria.copy(requiredProperty = Some("property2")), Some(Metadata("property2", ""))) should be(Some(REQUIRED_PROPERTY_IS_EMPTY)) + } + + } +} diff --git a/src/test/scala/uk/gov/nationalarchives/tdr/validation/MetadataValidationSpec.scala b/src/test/scala/uk/gov/nationalarchives/tdr/validation/MetadataValidationSpec.scala new file mode 100644 index 0000000..482d3ef --- /dev/null +++ b/src/test/scala/uk/gov/nationalarchives/tdr/validation/MetadataValidationSpec.scala @@ -0,0 +1,231 @@ +package uk.gov.nationalarchives.tdr.validation + +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers._ +import uk.gov.nationalarchives.tdr.validation.ErrorCode._ +import uk.gov.nationalarchives.tdr.validation.MetadataProperty.closureType + +class MetadataValidationSpec extends AnyFlatSpec { + + "validateClosureMetadata" should "validate closure metadata with valid values" in { + + val dependentMetadataCriteria = List( + MetadataCriteria("Property1", Boolean, true, false, false, List("yes", "no"), None, None), + MetadataCriteria("Property2", Text, true, false, false, Nil, None, None), + MetadataCriteria("Property3", DateTime, true, false, false, Nil, None, None), + MetadataCriteria("Property4", Integer, true, false, false, Nil, None, None) + ) + val closureMetadataCriteria = MetadataCriteria("ClosureType", Boolean, true, false, false, List("yes", "no"), None, Some(Map("Closed" -> dependentMetadataCriteria)), None) + val input = List( + Metadata("ClosureType", "Closed"), + Metadata("Property1", "yes"), + Metadata("Property2", "hello"), + Metadata("Property3", "1990-10-10T00:00:00"), + Metadata("Property4", "90") + ) + val metadataValidation = new MetadataValidation(closureMetadataCriteria, Nil) + val error = metadataValidation.validateClosureMetadata(input) + error should be(Nil) + } + + "validateClosureMetadata" should "return an error if closure type is empty" in { + + val dependentMetadataCriteria = List( + MetadataCriteria("Property1", Boolean, true, false, false, List("yes", "no"), None, None) + ) + val closureMetadataCriteria = MetadataCriteria("ClosureType", Boolean, true, false, false, List("yes", "no"), None, Some(Map("Closed" -> dependentMetadataCriteria)), None) + val input = List( + Metadata("ClosureType", ""), + Metadata("Property1", "yes") + ) + val metadataValidation = new MetadataValidation(closureMetadataCriteria, Nil) + val error = metadataValidation.validateClosureMetadata(input) + error should be(List(Error(closureType, UNDEFINED_VALUE_ERROR))) + } + + "validateClosureMetadata" should "return an error if closure type is missing" in { + + val dependentMetadataCriteria = List( + MetadataCriteria("Property1", Boolean, true, false, false, List("yes", "no"), None, None) + ) + val closureMetadataCriteria = MetadataCriteria("ClosureType", Boolean, true, false, false, List("yes", "no"), None, Some(Map("Closed" -> dependentMetadataCriteria)), None) + val input = List( + Metadata("Property1", "yes") + ) + val metadataValidation = new MetadataValidation(closureMetadataCriteria, Nil) + val error = metadataValidation.validateClosureMetadata(input) + error should be(List(Error(closureType, CLOSURE_STATUS_IS_MISSING))) + } + + "validateClosureMetadata" should "not return any error if closure status is open and closure metadata has empty or default value" in { + + val dependentMetadataCriteria = List( + MetadataCriteria("Property1", Boolean, true, false, false, List("yes", "no"), defaultValue = Some("no")), + MetadataCriteria("Property2", Text, true, false, false, Nil, None, None) + ) + val closureMetadataCriteria = MetadataCriteria("ClosureType", Boolean, true, false, false, List("yes", "no"), None, Some(Map("Closed" -> dependentMetadataCriteria)), None) + val input = List( + Metadata("ClosureType", "Open"), + Metadata("Property1", "no"), + Metadata("Property2", "") + ) + val metadataValidation = new MetadataValidation(closureMetadataCriteria, Nil) + val error = metadataValidation.validateClosureMetadata(input) + error should be(Nil) + } + + "validateClosureMetadata" should "return an error if closure status is open but has closure metadata" in { + + val dependentMetadataCriteria = List( + MetadataCriteria("Property1", Boolean, true, false, false, List("yes", "no"), None, None), + MetadataCriteria("Property2", Text, true, false, false, Nil, None, None) + ) + val closureMetadataCriteria = MetadataCriteria("ClosureType", Boolean, true, false, false, List("yes", "no"), None, Some(Map("Closed" -> dependentMetadataCriteria)), None) + val input = List( + Metadata("ClosureType", "Open"), + Metadata("Property1", "yes"), + Metadata("Property2", "hello") + ) + val metadataValidation = new MetadataValidation(closureMetadataCriteria, Nil) + val error = metadataValidation.validateClosureMetadata(input) + error should be(List(Error(closureType, CLOSURE_METADATA_EXISTS_WHEN_FILE_IS_OPEN))) + } + + "validateClosureMetadata" should "return an error if closure metadata has invalid values" in { + + val dependentMetadataCriteria = List( + MetadataCriteria("Property1", Boolean, true, false, false, List("yes", "no"), None, None), + MetadataCriteria("Property2", Text, true, false, false, Nil, None, None), + MetadataCriteria("Property3", DateTime, true, false, false, Nil, None, None), + MetadataCriteria("Property4", Integer, true, false, false, Nil, None, None) + ) + val closureMetadataCriteria = MetadataCriteria("ClosureType", Boolean, true, false, false, List("yes", "no"), None, Some(Map("Closed" -> dependentMetadataCriteria)), None) + val input = List( + Metadata("ClosureType", "Closed"), + Metadata("Property1", ""), + Metadata("Property2", ""), + Metadata("Property3", "1990-55-10T00:00:00"), + Metadata("Property4", "tt") + ) + val metadataValidation = new MetadataValidation(closureMetadataCriteria, Nil) + val error = metadataValidation.validateClosureMetadata(input) + error should be( + List( + Error("Property1", NO_OPTION_SELECTED_ERROR), + Error("Property2", EMPTY_VALUE_ERROR), + Error("Property3", INVALID_NUMBER_ERROR_FOR_MONTH), + Error("Property4", NUMBER_ONLY_ERROR) + ) + ) + } + + "validateClosureMetadata" should "return an error if dependent closure metadata has invalid values" in { + + val dependentMetadataCriteria = List( + MetadataCriteria( + "Property1", + Boolean, + true, + false, + false, + List("yes", "no"), + dependencies = Some(Map("yes" -> List(MetadataCriteria("Property11", Text, true, false, false, Nil)))) + ), + MetadataCriteria("Property2", Text, true, false, false, Nil, None, None), + MetadataCriteria("Property4", Integer, true, false, false, Nil, None, None) + ) + val closureMetadataCriteria = MetadataCriteria("ClosureType", Boolean, true, false, false, List("yes", "no"), None, Some(Map("Closed" -> dependentMetadataCriteria)), None) + val input = List( + Metadata("ClosureType", "Closed"), + Metadata("Property1", "yes"), + Metadata("Property11", ""), + Metadata("Property2", ""), + Metadata("Property4", "") + ) + val metadataValidation = new MetadataValidation(closureMetadataCriteria, Nil) + val error = metadataValidation.validateClosureMetadata(input) + error should be( + List( + Error("Property1", EMPTY_VALUE_ERROR), + Error("Property2", EMPTY_VALUE_ERROR), + Error("Property4", EMPTY_VALUE_ERROR) + ) + ) + } + + "validateClosureMetadata" should "not return an error if a closure metadata dependent by a descriptive metadata which is empty" in { + + val dependentMetadataCriteria = List( + MetadataCriteria( + "Property1", + Boolean, + true, + false, + false, + List("yes", "no"), + requiredProperty = Some("Property5"), + dependencies = Some(Map("yes" -> List(MetadataCriteria("Property11", Text, true, false, false, Nil)))) + ) + ) + val closureMetadataCriteria = MetadataCriteria("ClosureType", Boolean, true, false, false, List("yes", "no"), None, Some(Map("Closed" -> dependentMetadataCriteria)), None) + val input = List( + Metadata("ClosureType", "Closed"), + Metadata("Property1", ""), + Metadata("Property11", ""), + Metadata("Property5", "") + ) + val metadataValidation = new MetadataValidation(closureMetadataCriteria, Nil) + val error = metadataValidation.validateClosureMetadata(input) + error should be(Nil) + } + + "validateDescriptiveMetadata" should "validate descriptive metadata with valid values" in { + + val descriptiveMetadataCriteria = List( + MetadataCriteria("Property1", Text, false, false, false, Nil, None, None), + MetadataCriteria("Property2", Text, false, false, false, Nil, None, None), + MetadataCriteria("Property3", Text, false, false, false, Nil, None, None) + ) + val closureMetadataCriteria = MetadataCriteria("ClosureType", Boolean, true, false, false, List("yes", "no")) + val input = List( + Metadata("Property1", ""), + Metadata("Property2", ""), + Metadata("Property3", "test") + ) + val metadataValidation = new MetadataValidation(closureMetadataCriteria, descriptiveMetadataCriteria) + val error = metadataValidation.validateDescriptiveMetadata(input) + error should be(Nil) + } + + "validateMetadata" should "validate closure & descriptive metadata" in { + + val descriptiveMetadataCriteria = List( + MetadataCriteria("Property21", Text, false, false, false, Nil, None, None), + MetadataCriteria("Property22", Boolean, false, false, false, List("yes", "no"), None, None) + ) + val dependentMetadataCriteria = List( + MetadataCriteria("Property1", Boolean, true, false, false, List("yes", "no"), None, None), + MetadataCriteria("Property2", Text, true, false, false, Nil, None, None) + ) + val closureMetadataCriteria = MetadataCriteria("ClosureType", Boolean, true, false, false, List("yes", "no"), None, Some(Map("Closed" -> dependentMetadataCriteria)), None) + val metadata = List( + Metadata("ClosureType", "Closed"), + Metadata("Property1", ""), + Metadata("Property2", ""), + Metadata("Property21", ""), + Metadata("Property22", "hh") + ) + val fileRows = FileRow("file1", metadata) + val metadataValidation = new MetadataValidation(closureMetadataCriteria, descriptiveMetadataCriteria) + val error = metadataValidation.validateMetadata(List(fileRows)) + error should be( + Map( + "file1" -> List( + Error("Property1", NO_OPTION_SELECTED_ERROR), + Error("Property2", EMPTY_VALUE_ERROR), + Error("Property22", UNDEFINED_VALUE_ERROR) + ) + ) + ) + } +}