diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt index 01ba3531be..f5573a33be 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/QuestionnaireViewModel.kt @@ -72,8 +72,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.withIndex import kotlinx.coroutines.launch -import org.hl7.fhir.r4.model.Base -import org.hl7.fhir.r4.model.Element import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.Questionnaire.QuestionnaireItemComponent import org.hl7.fhir.r4.model.QuestionnaireResponse @@ -735,11 +733,11 @@ internal class QuestionnaireViewModel(application: Application, state: SavedStat questionnaireItem, questionnaireResponseItem.answer, this@QuestionnaireViewModel.getApplication(), - ) { _, expression -> + ) { expressionEvaluator.evaluateExpressionValue( questionnaireItem, questionnaireResponseItem, - expression, + it, ) } } else { diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt index 61be1b7f93..2a224a10b6 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/ExpressionEvaluator.kt @@ -142,7 +142,7 @@ internal class ExpressionEvaluator( } /** Returns the evaluation result of an expression as a [Type] value */ - fun evaluateExpressionValue( + suspend fun evaluateExpressionValue( questionnaireItem: QuestionnaireItemComponent, questionnaireResponseItem: QuestionnaireResponseItemComponent?, expression: Expression, diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt index 4c7d626913..1e86e4b8fd 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/fhirpath/FhirPathUtil.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 Google LLC + * Copyright 2022-2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import org.hl7.fhir.r4.model.ExpressionNode import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.QuestionnaireResponse.QuestionnaireResponseItemComponent import org.hl7.fhir.r4.model.Resource -import org.hl7.fhir.r4.model.Type import org.hl7.fhir.r4.utils.FHIRPathEngine private val fhirPathEngine: FHIRPathEngine = @@ -101,9 +100,9 @@ internal fun evaluateToBase( } /** Evaluates the given expression and returns list of [Base] */ -internal fun evaluateToBase(type: Type, expression: String): List { +internal fun evaluateToBase(base: Base, expression: String): List { return fhirPathEngine.evaluate( - /* base = */ type, + /* base = */ base, /* path = */ expression, ) } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerConstraintValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerConstraintValidator.kt index c73094790d..20759607af 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerConstraintValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerConstraintValidator.kt @@ -18,7 +18,6 @@ package com.google.android.fhir.datacapture.validation import android.content.Context import org.hl7.fhir.r4.model.Expression -import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Type @@ -38,11 +37,11 @@ internal interface AnswerConstraintValidator { * * [Learn more](https://www.hl7.org/fhir/questionnaireresponse.html#link). */ - fun validate( + suspend fun validate( questionnaireItem: Questionnaire.QuestionnaireItemComponent, answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent, context: Context, - evaluateExtensionCqfCalculatedValue: (Extension, Expression) -> Type?, + expressionEvaluator: suspend (Expression) -> Type?, ): Result /** diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerExtensionConstraintValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerExtensionConstraintValidator.kt index 1d85275fc7..5e5ed9734d 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerExtensionConstraintValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/AnswerExtensionConstraintValidator.kt @@ -20,7 +20,6 @@ import android.content.Context import com.google.android.fhir.datacapture.extensions.cqfCalculatedValueExpression import com.google.android.fhir.datacapture.extensions.hasValue import org.hl7.fhir.r4.model.Expression -import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Type @@ -40,38 +39,31 @@ internal open class AnswerExtensionConstraintValidator( val url: String, val predicate: ( - /*extensionValue*/ + /*constraintValue*/ Type, - /*answer*/ QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent, ) -> Boolean, val messageGenerator: (Type, Context) -> String, ) : AnswerConstraintValidator { - override fun validate( + override suspend fun validate( questionnaireItem: Questionnaire.QuestionnaireItemComponent, answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent, context: Context, - evaluateExtensionCqfCalculatedValue: (Extension, Expression) -> Type?, + expressionEvaluator: suspend (Expression) -> Type?, ): AnswerConstraintValidator.Result { if (questionnaireItem.hasExtension(url)) { val extension = questionnaireItem.getExtensionByUrl(url) - val extensionValueType = - extension.value.let { - it.cqfCalculatedValueExpression?.let { expression -> - evaluateExtensionCqfCalculatedValue(extension, expression) - } - ?: it - } + val extensionCalculatedValue = + extension.value.cqfCalculatedValueExpression?.let { expressionEvaluator(it) } + val extensionValue = extensionCalculatedValue ?: extension.value // Only checks constraint if both extension and answer have a value if ( - extensionValueType.hasValue() && - answer.value.hasValue() && - predicate(extensionValueType, answer) + extensionValue.hasValue() && answer.value.hasValue() && predicate(extensionValue, answer) ) { return AnswerConstraintValidator.Result( false, - messageGenerator(extensionValueType, context), + messageGenerator(extensionValue, context), ) } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidator.kt index 4b08c5b685..fdfc088be6 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidator.kt @@ -33,16 +33,16 @@ internal object MaxDecimalPlacesValidator : AnswerExtensionConstraintValidator( url = MAX_DECIMAL_URL, predicate = { - extensionValue: Type, + constraintValue: Type, answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent, -> - val maxDecimalPlaces = (extensionValue as? IntegerType)?.value + val maxDecimalPlaces = (constraintValue as? IntegerType)?.value answer.hasValueDecimalType() && maxDecimalPlaces != null && answer.valueDecimalType.valueAsString.substringAfter(".").length > maxDecimalPlaces }, - messageGenerator = { extensionValue: Type, context: Context -> - context.getString(R.string.max_decimal_validation_error_msg, extensionValue.primitiveValue()) + messageGenerator = { constraintValue: Type, context: Context -> + context.getString(R.string.max_decimal_validation_error_msg, constraintValue.primitiveValue()) }, ) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxLengthValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxLengthValidator.kt index 09c9b365e3..0d3fb6aa55 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxLengthValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxLengthValidator.kt @@ -19,7 +19,6 @@ package com.google.android.fhir.datacapture.validation import android.content.Context import com.google.android.fhir.datacapture.extensions.asStringValue import org.hl7.fhir.r4.model.Expression -import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Type @@ -31,11 +30,11 @@ import org.hl7.fhir.r4.model.Type * https://www.hl7.org/fhir/valueset-item-type.html#expansion */ internal object MaxLengthValidator : AnswerConstraintValidator { - override fun validate( + override suspend fun validate( questionnaireItem: Questionnaire.QuestionnaireItemComponent, answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent, context: Context, - evaluateExtensionCqfCalculatedValue: (Extension, Expression) -> Type?, + expressionEvaluator: suspend (Expression) -> Type?, ): AnswerConstraintValidator.Result { if ( questionnaireItem.hasMaxLength() && diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxValueValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxValueValidator.kt index 5f81a764f3..3bd12d4788 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxValueValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MaxValueValidator.kt @@ -30,15 +30,15 @@ internal object MaxValueValidator : AnswerExtensionConstraintValidator( url = MAX_VALUE_EXTENSION_URL, predicate = { - extensionValue: Type, + constraintValue: Type, answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent, -> - answer.value > extensionValue + answer.value > constraintValue }, - messageGenerator = { extensionValue: Type, context: Context -> + messageGenerator = { constraintValue: Type, context: Context -> context.getString( R.string.max_value_validation_error_msg, - extensionValue.getValueAsString(context), + constraintValue.getValueAsString(context), ) }, ) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinLengthValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinLengthValidator.kt index cc9a9741b0..623554b2f9 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinLengthValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinLengthValidator.kt @@ -37,13 +37,13 @@ import org.hl7.fhir.r4.model.Type internal object MinLengthValidator : AnswerExtensionConstraintValidator( url = MIN_LENGTH_EXTENSION_URL, - predicate = { extensionValue, answer -> + predicate = { constraintValue, answer -> answer.value.isPrimitive && (answer.value as PrimitiveType<*>).asStringValue().length < - (extensionValue as IntegerType).value + (constraintValue as IntegerType).value }, - messageGenerator = { extensionValue: Type, context: Context -> - context.getString(R.string.min_length_validation_error_msg, extensionValue.primitiveValue()) + messageGenerator = { constraintValue: Type, context: Context -> + context.getString(R.string.min_length_validation_error_msg, constraintValue.primitiveValue()) }, ) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinValueValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinValueValidator.kt index 6fd92ed65e..5cc851e394 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinValueValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/MinValueValidator.kt @@ -30,15 +30,15 @@ internal object MinValueValidator : AnswerExtensionConstraintValidator( url = MIN_VALUE_EXTENSION_URL, predicate = { - extensionValue: Type, + constraintValue: Type, answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent, -> - answer.value < extensionValue + answer.value < constraintValue }, - messageGenerator = { extensionValue: Type, context: Context -> + messageGenerator = { constraintValue: Type, context: Context -> context.getString( R.string.min_value_validation_error_msg, - extensionValue.getValueAsString(context), + constraintValue.getValueAsString(context), ) }, ) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidator.kt index 9af989cada..7a386e48b2 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidator.kt @@ -19,7 +19,6 @@ package com.google.android.fhir.datacapture.validation import android.content.Context import com.google.android.fhir.datacapture.extensions.isHidden import org.hl7.fhir.r4.model.Expression -import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.Questionnaire import org.hl7.fhir.r4.model.QuestionnaireResponse import org.hl7.fhir.r4.model.Type @@ -44,11 +43,11 @@ internal object QuestionnaireResponseItemValidator { ) /** Validates [answers] contains valid answer(s) to [questionnaireItem]. */ - fun validate( + suspend fun validate( questionnaireItem: Questionnaire.QuestionnaireItemComponent, answers: List, context: Context, - expressionValueEvaluator: (Extension, Expression) -> Type?, + expressionEvaluator: suspend (Expression) -> Type?, ): ValidationResult { if (questionnaireItem.isHidden) return NotValidated @@ -59,9 +58,7 @@ internal object QuestionnaireResponseItemValidator { val questionnaireResponseItemAnswerConstraintValidationResult = answerConstraintValidators.flatMap { validator -> answers.map { answer -> - validator.validate(questionnaireItem, answer, context) { extension, expression -> - expressionValueEvaluator.invoke(extension, expression) - } + validator.validate(questionnaireItem, answer, context, expressionEvaluator) } } diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt index 3fa95e3d91..ec449b36d9 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseValidator.kt @@ -181,11 +181,11 @@ object QuestionnaireResponseValidator { questionnaireItem, questionnaireResponseItem.answer, context, - ) { _, expression -> + ) { expressionEvaluator.evaluateExpressionValue( questionnaireItem, questionnaireResponseItem, - expression, + it, ) }, ) diff --git a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/RegexValidator.kt b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/RegexValidator.kt index 04a06cad08..97d1a6f56d 100644 --- a/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/RegexValidator.kt +++ b/datacapture/src/main/java/com/google/android/fhir/datacapture/validation/RegexValidator.kt @@ -37,22 +37,22 @@ internal object RegexValidator : url = REGEX_EXTENSION_URL, predicate = predicate@{ - extensionValue: Type, + constraintValue: Type, answer: QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent, -> - if (!extensionValue.isPrimitive || !answer.value.isPrimitive) { + if (!constraintValue.isPrimitive || !answer.value.isPrimitive) { return@predicate false } try { - val pattern = Pattern.compile((extensionValue as PrimitiveType<*>).asStringValue()) + val pattern = Pattern.compile((constraintValue as PrimitiveType<*>).asStringValue()) !pattern.matcher(answer.value.asStringValue()).matches() } catch (e: PatternSyntaxException) { - Timber.w("Can't parse regex: $extensionValue", e) + Timber.w("Can't parse regex: $constraintValue", e) false } }, - messageGenerator = { extensionValue: Type, context: Context -> - context.getString(R.string.regex_validation_error_msg, extensionValue.primitiveValue()) + messageGenerator = { constraintValue: Type, context: Context -> + context.getString(R.string.regex_validation_error_msg, constraintValue.primitiveValue()) }, ) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidatorTest.kt index aba8340b0f..52b3f9f1ab 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxDecimalPlacesValidatorTest.kt @@ -20,6 +20,7 @@ import android.content.Context import android.os.Build import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.DecimalType import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.IntegerType @@ -42,15 +43,16 @@ class MaxDecimalPlacesValidatorTest { } @Test - fun validate_noExtension_shouldReturnValidResult() { + fun validate_noExtension_shouldReturnValidResult() = runTest { + val questionnaireItem = Questionnaire.QuestionnaireItemComponent() val validationResult = MaxDecimalPlacesValidator.validate( - Questionnaire.QuestionnaireItemComponent(), + questionnaireItem, QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() .setValue(DecimalType("1.00")), context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + ) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) } assertThat(validationResult.isValid).isTrue() @@ -58,17 +60,19 @@ class MaxDecimalPlacesValidatorTest { } @Test - fun validate_validAnswer_shouldReturnValidResult() { + fun validate_validAnswer_shouldReturnValidResult() = runTest { + val questionnaireItem = + Questionnaire.QuestionnaireItemComponent().apply { + this.addExtension(Extension(MAX_DECIMAL_URL, IntegerType(2))) + } val validationResult = MaxDecimalPlacesValidator.validate( - Questionnaire.QuestionnaireItemComponent().apply { - this.addExtension(Extension(MAX_DECIMAL_URL, IntegerType(2))) - }, + questionnaireItem, QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() .setValue(DecimalType("1.00")), context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + ) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) } assertThat(validationResult.isValid).isTrue() @@ -76,17 +80,19 @@ class MaxDecimalPlacesValidatorTest { } @Test - fun validate_tooManyDecimalPlaces_shouldReturnInvalidResult() { + fun validate_tooManyDecimalPlaces_shouldReturnInvalidResult() = runTest { + val questionnaireItem = + Questionnaire.QuestionnaireItemComponent().apply { + this.addExtension(Extension(MAX_DECIMAL_URL, IntegerType(2))) + } val validationResult = MaxDecimalPlacesValidator.validate( - Questionnaire.QuestionnaireItemComponent().apply { - this.addExtension(Extension(MAX_DECIMAL_URL, IntegerType(2))) - }, + questionnaireItem, QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent() .setValue(DecimalType("1.000")), context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + ) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) } assertThat(validationResult.isValid).isFalse() diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxLengthValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxLengthValidatorTest.kt index 58d56ec773..cb9edda933 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxLengthValidatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxLengthValidatorTest.kt @@ -22,6 +22,7 @@ import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import java.net.URI import java.text.SimpleDateFormat +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.BooleanType import org.hl7.fhir.r4.model.DateType import org.hl7.fhir.r4.model.DecimalType @@ -51,79 +52,79 @@ class MaxLengthValidatorTest { } @Test - fun boolean_answerOverMaxLength_shouldReturnInvalidResult() { + fun boolean_answerOverMaxLength_shouldReturnInvalidResult() = runTest { checkAnswerOverMaxLength(maxLength = 4, value = BooleanType(false)) } @Test - fun boolean_answerUnderMaxLength_shouldReturnValidResult() { + fun boolean_answerUnderMaxLength_shouldReturnValidResult() = runTest { checkAnswerUnderMaxLength(maxLength = 6, value = BooleanType(false)) } @Test - fun decimal_answerOverMaxLength_shouldReturnInvalidResult() { + fun decimal_answerOverMaxLength_shouldReturnInvalidResult() = runTest { checkAnswerOverMaxLength(maxLength = 10, value = DecimalType(3.1415926535)) } @Test - fun decimal_answerUnderMaxLength_shouldReturnValidResult() { + fun decimal_answerUnderMaxLength_shouldReturnValidResult() = runTest { checkAnswerUnderMaxLength(maxLength = 16, value = DecimalType(3.1415926535)) } @Test - fun int_answerOverMaxLength_shouldReturnInvalidResult() { + fun int_answerOverMaxLength_shouldReturnInvalidResult() = runTest { checkAnswerOverMaxLength(maxLength = 5, value = IntegerType(1234567890)) } @Test - fun int_answerUnderMaxLength_shouldReturnValidResult() { + fun int_answerUnderMaxLength_shouldReturnValidResult() = runTest { checkAnswerUnderMaxLength(maxLength = 10, value = IntegerType(1234567890)) } @Test - fun dateType_answerOverMaxLength_shouldReturnInvalidResult() { + fun dateType_answerOverMaxLength_shouldReturnInvalidResult() = runTest { val dateFormat = SimpleDateFormat("yyyy-MM-dd") checkAnswerOverMaxLength(maxLength = 5, value = DateType(dateFormat.parse("2021-06-01"))) } @Test - fun date_answerUnderMaxLength_shouldReturnValidResult() { + fun date_answerUnderMaxLength_shouldReturnValidResult() = runTest { val dateFormat = SimpleDateFormat("yyyy-MM-dd") checkAnswerUnderMaxLength(maxLength = 11, value = DateType(dateFormat.parse("2021-06-01"))) } @Test - fun time_answerOverMaxLength_shouldReturnInvalidResult() { + fun time_answerOverMaxLength_shouldReturnInvalidResult() = runTest { checkAnswerOverMaxLength(maxLength = 5, value = TimeType("18:00:59")) } @Test - fun time_answerUnderMaxLength_shouldReturnValidResult() { + fun time_answerUnderMaxLength_shouldReturnValidResult() = runTest { checkAnswerUnderMaxLength(maxLength = 9, value = TimeType("18:00:59")) } @Test - fun string_answerOverMaxLength_shouldReturnInvalidResult() { + fun string_answerOverMaxLength_shouldReturnInvalidResult() = runTest { checkAnswerOverMaxLength(maxLength = 5, value = StringType("Hello World")) } @Test - fun string_answerUnderMaxLength_shouldReturnValidResult() { + fun string_answerUnderMaxLength_shouldReturnValidResult() = runTest { checkAnswerUnderMaxLength(maxLength = 11, value = StringType("Hello World")) } @Test - fun uri_answerOverMaxLength_shouldReturnInvalidResult() { + fun uri_answerOverMaxLength_shouldReturnInvalidResult() = runTest { checkAnswerOverMaxLength(maxLength = 5, value = UriType(URI.create("https://www.hl7.org/"))) } @Test - fun uri_answerUnderMaxLength_shouldReturnValidResult() { + fun uri_answerUnderMaxLength_shouldReturnValidResult() = runTest { checkAnswerUnderMaxLength(maxLength = 20, value = UriType(URI.create("https://www.hl7.org/"))) } @Test - fun nonPrimitiveOverMaxLength_shouldReturnValidResult() { + fun nonPrimitiveOverMaxLength_shouldReturnValidResult() = runTest { val requirement = Questionnaire.QuestionnaireItemComponent().apply { maxLength = 5 } val answer = QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { @@ -131,8 +132,8 @@ class MaxLengthValidatorTest { } val validationResult = - MaxLengthValidator.validate(requirement, answer, context) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + MaxLengthValidator.validate(requirement, answer, context) { + TestExpressionValueEvaluator.evaluate(requirement, it) } assertThat(validationResult.isValid).isTrue() @@ -144,7 +145,7 @@ class MaxLengthValidatorTest { var context: Context = ApplicationProvider.getApplicationContext() @JvmStatic - fun checkAnswerOverMaxLength(maxLength: Int, value: PrimitiveType<*>) { + suspend fun checkAnswerOverMaxLength(maxLength: Int, value: PrimitiveType<*>) { val testComponent = createMaxLengthQuestionnaireTestItem(maxLength, value) val validationResult = @@ -152,8 +153,8 @@ class MaxLengthValidatorTest { testComponent.requirement, testComponent.answer, context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + ) { + TestExpressionValueEvaluator.evaluate(testComponent.requirement, it) } assertThat(validationResult.isValid).isFalse() @@ -164,7 +165,7 @@ class MaxLengthValidatorTest { } @JvmStatic - fun checkAnswerUnderMaxLength(maxLength: Int, value: PrimitiveType<*>) { + suspend fun checkAnswerUnderMaxLength(maxLength: Int, value: PrimitiveType<*>) { val testComponent = createMaxLengthQuestionnaireTestItem(maxLength, value) val validationResult = @@ -172,8 +173,8 @@ class MaxLengthValidatorTest { testComponent.requirement, testComponent.answer, context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + ) { + TestExpressionValueEvaluator.evaluate(testComponent.requirement, it) } assertThat(validationResult.isValid).isTrue() diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxValueValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxValueValidatorTest.kt index 8c9e4df18d..2c3b1346bd 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxValueValidatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MaxValueValidatorTest.kt @@ -25,6 +25,7 @@ import com.google.common.truth.Truth.assertThat import java.time.LocalDate import java.time.ZoneId import java.util.Date +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.DateType import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Extension @@ -49,7 +50,7 @@ class MaxValueValidatorTest { } @Test - fun `should return invalid result when entered value is greater than maxValue`() { + fun `should return invalid result when entered value is greater than maxValue`() = runTest { val questionnaireItem = Questionnaire.QuestionnaireItemComponent().apply { addExtension( @@ -65,8 +66,8 @@ class MaxValueValidatorTest { } val validationResult = - MaxValueValidator.validate(questionnaireItem, answer, context) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + MaxValueValidator.validate(questionnaireItem, answer, context) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) } assertThat(validationResult.isValid).isFalse() @@ -74,7 +75,7 @@ class MaxValueValidatorTest { } @Test - fun `should return valid result when entered value is less than maxValue`() { + fun `should return valid result when entered value is less than maxValue`() = runTest { val questionnaireItem = Questionnaire.QuestionnaireItemComponent().apply { addExtension( @@ -90,8 +91,8 @@ class MaxValueValidatorTest { } val validationResult = - MaxValueValidator.validate(questionnaireItem, answer, context) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + MaxValueValidator.validate(questionnaireItem, answer, context) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) } assertThat(validationResult.isValid).isTrue() @@ -99,166 +100,172 @@ class MaxValueValidatorTest { } @Test - fun `should return invalid result with correct max allowed value if contains only cqf-calculatedValue`() { - val questionnaireItem = - Questionnaire.QuestionnaireItemComponent().apply { - addExtension( - Extension().apply { - this.url = MAX_VALUE_EXTENSION_URL - this.setValue( - DateType().apply { - addExtension( - Extension( - EXTENSION_CQF_CALCULATED_VALUE_URL, - Expression().apply { - expression = "today() - 7 'days'" - language = "text/fhirpath" - }, - ), - ) - }, - ) - }, - ) - } - val answer = - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = DateType().apply { value = Date() } - } - - val validationResult = - MaxValueValidator.validate(questionnaireItem, answer, context) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) - } - - assertThat(validationResult.isValid).isFalse() - assertThat(validationResult.errorMessage) - .isEqualTo("Maximum value allowed is:${LocalDate.now().minusDays(7)}") - } - - @Test - fun `should return invalid result with correct max allowed value if contains both value and cqf-calculatedValue`() { - val tenDaysAgo = LocalDate.now().minusDays(10) - - val questionnaireItem = - Questionnaire.QuestionnaireItemComponent().apply { - addExtension( - Extension().apply { - this.url = MAX_VALUE_EXTENSION_URL - this.setValue( - DateType().apply { - value = - Date.from(tenDaysAgo.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()) - addExtension( - Extension( - EXTENSION_CQF_CALCULATED_VALUE_URL, - Expression().apply { - expression = "today() - 7 'days'" - language = "text/fhirpath" - }, - ), - ) - }, - ) - }, - ) - } - val answer = - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = DateType().apply { value = Date() } - } - - val validationResult = - MaxValueValidator.validate(questionnaireItem, answer, context) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) - } - - assertThat(validationResult.isValid).isFalse() - assertThat(validationResult.errorMessage) - .isEqualTo("Maximum value allowed is:${LocalDate.now().minusDays(7)}") - } - - @Test - fun `should return valid result and removes constraint for an answer value when maxValue cqf-calculatedValue evaluates to empty`() { - val questionnaireItem = - Questionnaire.QuestionnaireItemComponent().apply { - addExtension( - Extension().apply { - url = MAX_VALUE_EXTENSION_URL - this.setValue( - DateType().apply { - extension = - listOf( + fun `should return invalid result with correct max allowed value if contains only cqf-calculatedValue`() = + runTest { + val questionnaireItem = + Questionnaire.QuestionnaireItemComponent().apply { + addExtension( + Extension().apply { + this.url = MAX_VALUE_EXTENSION_URL + this.setValue( + DateType().apply { + addExtension( Extension( EXTENSION_CQF_CALCULATED_VALUE_URL, Expression().apply { + expression = "today() - 7 'days'" language = "text/fhirpath" - expression = "yesterday()" // invalid FHIRPath expression }, ), ) - }, - ) - }, - ) - } - - val answer = - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = DateType(Date()) - } + }, + ) + }, + ) + } + val answer = + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = DateType().apply { value = Date() } + } - val validationResult = - MaxValueValidator.validate( - questionnaireItem, - answer, - InstrumentationRegistry.getInstrumentation().context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) - } + val validationResult = + MaxValueValidator.validate(questionnaireItem, answer, context) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) + } - assertThat(validationResult.isValid).isTrue() - assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue() - } + assertThat(validationResult.isValid).isFalse() + assertThat(validationResult.errorMessage) + .isEqualTo("Maximum value allowed is:${LocalDate.now().minusDays(7)}") + } @Test - fun `should return valid result and removes constraint for an answer with an empty value`() { - val questionnaireItem = - Questionnaire.QuestionnaireItemComponent().apply { - addExtension( - Extension().apply { - url = MAX_VALUE_EXTENSION_URL - this.setValue( - DateType().apply { - extension = - listOf( + fun `should return invalid result with correct max allowed value if contains both value and cqf-calculatedValue`() = + runTest { + val tenDaysAgo = LocalDate.now().minusDays(10) + + val questionnaireItem = + Questionnaire.QuestionnaireItemComponent().apply { + addExtension( + Extension().apply { + this.url = MAX_VALUE_EXTENSION_URL + this.setValue( + DateType().apply { + value = + Date.from(tenDaysAgo.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()) + addExtension( Extension( EXTENSION_CQF_CALCULATED_VALUE_URL, Expression().apply { + expression = "today() - 7 'days'" language = "text/fhirpath" - expression = "today()" }, ), ) - }, - ) - }, - ) - } + }, + ) + }, + ) + } + val answer = + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = DateType().apply { value = Date() } + } - val answer = - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { value = DateType() } + val validationResult = + MaxValueValidator.validate(questionnaireItem, answer, context) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) + } - val validationResult = - MaxValueValidator.validate( - questionnaireItem, - answer, - InstrumentationRegistry.getInstrumentation().context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) - } + assertThat(validationResult.isValid).isFalse() + assertThat(validationResult.errorMessage) + .isEqualTo("Maximum value allowed is:${LocalDate.now().minusDays(7)}") + } - assertThat(validationResult.isValid).isTrue() - assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue() - } + @Test + fun `should return valid result and removes constraint for an answer value when maxValue cqf-calculatedValue evaluates to empty`() = + runTest { + val questionnaireItem = + Questionnaire.QuestionnaireItemComponent().apply { + addExtension( + Extension().apply { + url = MAX_VALUE_EXTENSION_URL + this.setValue( + DateType().apply { + extension = + listOf( + Extension( + EXTENSION_CQF_CALCULATED_VALUE_URL, + Expression().apply { + language = "text/fhirpath" + expression = "yesterday()" // invalid FHIRPath expression + }, + ), + ) + }, + ) + }, + ) + } + + val answer = + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = DateType(Date()) + } + + val validationResult = + MaxValueValidator.validate( + questionnaireItem, + answer, + InstrumentationRegistry.getInstrumentation().context, + ) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) + } + + assertThat(validationResult.isValid).isTrue() + assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue() + } + + @Test + fun `should return valid result and removes constraint for an answer with an empty value`() = + runTest { + val questionnaireItem = + Questionnaire.QuestionnaireItemComponent().apply { + addExtension( + Extension().apply { + url = MAX_VALUE_EXTENSION_URL + this.setValue( + DateType().apply { + extension = + listOf( + Extension( + EXTENSION_CQF_CALCULATED_VALUE_URL, + Expression().apply { + language = "text/fhirpath" + expression = "today()" + }, + ), + ) + }, + ) + }, + ) + } + + val answer = + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = DateType() + } + + val validationResult = + MaxValueValidator.validate( + questionnaireItem, + answer, + InstrumentationRegistry.getInstrumentation().context, + ) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) + } + + assertThat(validationResult.isValid).isTrue() + assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue() + } } diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinLengthValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinLengthValidatorTest.kt index 359f667bb3..c41ffbe49e 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinLengthValidatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinLengthValidatorTest.kt @@ -22,6 +22,7 @@ import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import java.net.URI import java.text.SimpleDateFormat +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.BooleanType import org.hl7.fhir.r4.model.DateType import org.hl7.fhir.r4.model.DecimalType @@ -52,79 +53,79 @@ class MinLengthValidatorTest { } @Test - fun boolean_answerUnderMinLength_shouldReturnInvalidResult() { + fun boolean_answerUnderMinLength_shouldReturnInvalidResult() = runTest { checkAnswerUnderMinLength(minLength = 10, value = BooleanType(false)) } @Test - fun boolean_answerOverMinLength_shouldReturnValidResult() { + fun boolean_answerOverMinLength_shouldReturnValidResult() = runTest { checkAnswerOverMinLength(minLength = 5, value = BooleanType(false)) } @Test - fun decimal_answerUnderMinLength_shouldReturnInvalidResult() { + fun decimal_answerUnderMinLength_shouldReturnInvalidResult() = runTest { checkAnswerUnderMinLength(minLength = 15, value = DecimalType(3.1415926535)) } @Test - fun decimal_answerOverMinLength_shouldReturnValidResult() { + fun decimal_answerOverMinLength_shouldReturnValidResult() = runTest { checkAnswerOverMinLength(minLength = 10, value = DecimalType(3.1415926535)) } @Test - fun int_answerUnderMinLength_shouldReturnInvalidResult() { + fun int_answerUnderMinLength_shouldReturnInvalidResult() = runTest { checkAnswerUnderMinLength(minLength = 5, value = IntegerType(123)) } @Test - fun int_answerOverMinLength_shouldReturnValidResult() { + fun int_answerOverMinLength_shouldReturnValidResult() = runTest { checkAnswerOverMinLength(minLength = 10, value = IntegerType(1234567890)) } @Test - fun dateType_answerUnderMinLength_shouldReturnInvalidResult() { + fun dateType_answerUnderMinLength_shouldReturnInvalidResult() = runTest { val dateFormat = SimpleDateFormat("yyyy-MM-dd") checkAnswerUnderMinLength(minLength = 11, value = DateType(dateFormat.parse("2021-06-01"))) } @Test - fun date_answerOverMinLength_shouldReturnValidResult() { + fun date_answerOverMinLength_shouldReturnValidResult() = runTest { val dateFormat = SimpleDateFormat("yyyy-MM-dd") checkAnswerOverMinLength(minLength = 10, value = DateType(dateFormat.parse("2021-06-01"))) } @Test - fun time_answerUnderMinLength_shouldReturnInvalidResult() { + fun time_answerUnderMinLength_shouldReturnInvalidResult() = runTest { checkAnswerUnderMinLength(minLength = 10, value = TimeType("18:00:59")) } @Test - fun time_answerOverMinLength_shouldReturnValidResult() { + fun time_answerOverMinLength_shouldReturnValidResult() = runTest { checkAnswerOverMinLength(minLength = 5, value = TimeType("18:00:59")) } @Test - fun string_answerUnderMinLength_shouldReturnInvalidResult() { + fun string_answerUnderMinLength_shouldReturnInvalidResult() = runTest { checkAnswerUnderMinLength(minLength = 12, value = StringType("Hello World")) } @Test - fun string_answerOverMinLength_shouldReturnValidResult() { + fun string_answerOverMinLength_shouldReturnValidResult() = runTest { checkAnswerOverMinLength(minLength = 5, value = StringType("Hello World")) } @Test - fun uri_answerUnderMinLength_shouldReturnInvalidResult() { + fun uri_answerUnderMinLength_shouldReturnInvalidResult() = runTest { checkAnswerUnderMinLength(minLength = 21, value = UriType(URI.create("https://www.hl7.org/"))) } @Test - fun uri_answerOverMinLength_shouldReturnValidResult() { + fun uri_answerOverMinLength_shouldReturnValidResult() = runTest { checkAnswerOverMinLength(minLength = 5, value = UriType(URI.create("https://www.hl7.org/"))) } @Test - fun nonPrimitiveUnderMinLength_shouldReturnValidResult() { + fun nonPrimitiveUnderMinLength_shouldReturnValidResult() = runTest { val requirement = Questionnaire.QuestionnaireItemComponent().apply { addExtension( @@ -140,8 +141,8 @@ class MinLengthValidatorTest { } val validationResult = - MaxLengthValidator.validate(requirement, answer, context) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + MaxLengthValidator.validate(requirement, answer, context) { + TestExpressionValueEvaluator.evaluate(requirement, it) } assertThat(validationResult.isValid).isTrue() @@ -153,7 +154,7 @@ class MinLengthValidatorTest { var context: Context = ApplicationProvider.getApplicationContext() @JvmStatic - fun checkAnswerOverMinLength(minLength: Int, value: PrimitiveType<*>) { + suspend fun checkAnswerOverMinLength(minLength: Int, value: PrimitiveType<*>) { val testComponent = createMaxLengthQuestionnaireTestItem(minLength, value) val validationResult = @@ -161,8 +162,8 @@ class MinLengthValidatorTest { testComponent.requirement, testComponent.answer, context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + ) { + TestExpressionValueEvaluator.evaluate(testComponent.requirement, it) } assertThat(validationResult.isValid).isTrue() @@ -170,7 +171,7 @@ class MinLengthValidatorTest { } @JvmStatic - fun checkAnswerUnderMinLength(minLength: Int, value: PrimitiveType<*>) { + suspend fun checkAnswerUnderMinLength(minLength: Int, value: PrimitiveType<*>) { val testComponent = createMaxLengthQuestionnaireTestItem(minLength, value) val validationResult = @@ -178,8 +179,8 @@ class MinLengthValidatorTest { testComponent.requirement, testComponent.answer, context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + ) { + TestExpressionValueEvaluator.evaluate(testComponent.requirement, it) } assertThat(validationResult.isValid).isFalse() diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinValueValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinValueValidatorTest.kt index 497feb3439..8622e7289e 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinValueValidatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/MinValueValidatorTest.kt @@ -25,6 +25,7 @@ import com.google.common.truth.Truth.assertThat import java.time.LocalDate import java.time.ZoneId import java.util.Date +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.DateType import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Extension @@ -50,7 +51,7 @@ class MinValueValidatorTest { } @Test - fun `should return invalid result when entered value is less than minValue`() { + fun `should return invalid result when entered value is less than minValue`() = runTest { val questionnaireItem = Questionnaire.QuestionnaireItemComponent().apply { addExtension( @@ -67,8 +68,8 @@ class MinValueValidatorTest { questionnaireItem, answer, InstrumentationRegistry.getInstrumentation().context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + ) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) } assertThat(validationResult.isValid).isFalse() @@ -76,7 +77,7 @@ class MinValueValidatorTest { } @Test - fun `should return valid result when entered value is greater than minValue`() { + fun `should return valid result when entered value is greater than minValue`() = runTest { val questionnaireItem = Questionnaire.QuestionnaireItemComponent().apply { addExtension( @@ -93,8 +94,8 @@ class MinValueValidatorTest { questionnaireItem, answer, InstrumentationRegistry.getInstrumentation().context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + ) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) } assertThat(validationResult.isValid).isTrue() @@ -102,172 +103,179 @@ class MinValueValidatorTest { } @Test - fun `should return valid result when entered value is greater than minValue for cqf calculated expression`() { - val questionnaireItem = - Questionnaire.QuestionnaireItemComponent().apply { - addExtension( - Extension().apply { - url = MIN_VALUE_EXTENSION_URL - this.setValue( - DateType().apply { - extension = - listOf( - Extension( - EXTENSION_CQF_CALCULATED_VALUE_URL, - Expression().apply { - language = "text/fhirpath" - expression = "today() - 1 'days'" - }, - ), - ) - }, - ) - }, - ) - } + fun `should return valid result when entered value is greater than minValue for cqf calculated expression`() = + runTest { + val questionnaireItem = + Questionnaire.QuestionnaireItemComponent().apply { + addExtension( + Extension().apply { + url = MIN_VALUE_EXTENSION_URL + this.setValue( + DateType().apply { + extension = + listOf( + Extension( + EXTENSION_CQF_CALCULATED_VALUE_URL, + Expression().apply { + language = "text/fhirpath" + expression = "today() - 1 'days'" + }, + ), + ) + }, + ) + }, + ) + } - val answer = QuestionnaireResponseItemAnswerComponent().apply { value = DateType(Date()) } + val answer = QuestionnaireResponseItemAnswerComponent().apply { value = DateType(Date()) } - val validationResult = - MinValueValidator.validate( - questionnaireItem, - answer, - InstrumentationRegistry.getInstrumentation().context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) - } + val validationResult = + MinValueValidator.validate( + questionnaireItem, + answer, + InstrumentationRegistry.getInstrumentation().context, + ) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) + } - assertThat(validationResult.isValid).isTrue() - assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue() - } + assertThat(validationResult.isValid).isTrue() + assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue() + } @Test - fun `should return invalid result with correct min allowed value if contains both value and cqf-calculatedValue`() { - val sevenDaysAgo = LocalDate.now().minusDays(7) - - val questionnaireItem = - Questionnaire.QuestionnaireItemComponent().apply { - addExtension( - Extension().apply { - this.url = MIN_VALUE_EXTENSION_URL - this.setValue( - DateType().apply { - value = - Date.from(sevenDaysAgo.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()) - addExtension( - Extension( - EXTENSION_CQF_CALCULATED_VALUE_URL, - Expression().apply { - expression = "today() - 3 'days'" - language = "text/fhirpath" - }, - ), - ) - }, - ) - }, - ) - } - val answer = - QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { - value = - DateType().apply { - val fiveDaysAgo = LocalDate.now().minusDays(5) - value = Date.from(fiveDaysAgo.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()) - } - } + fun `should return invalid result with correct min allowed value if contains both value and cqf-calculatedValue`() = + runTest { + val sevenDaysAgo = LocalDate.now().minusDays(7) - val validationResult = - MinValueValidator.validate(questionnaireItem, answer, context) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) - } - - assertThat(validationResult.isValid).isFalse() - assertThat(validationResult.errorMessage) - .isEqualTo("Minimum value allowed is:${LocalDate.now().minusDays(3)}") - } - - @Test - fun `should return valid result and removes constraint for an answer value when minValue cqf-calculatedValue evaluates to empty`() { - val questionnaireItem = - Questionnaire.QuestionnaireItemComponent().apply { - addExtension( - Extension().apply { - url = MIN_VALUE_EXTENSION_URL - this.setValue( - DateType().apply { - extension = - listOf( + val questionnaireItem = + Questionnaire.QuestionnaireItemComponent().apply { + addExtension( + Extension().apply { + this.url = MIN_VALUE_EXTENSION_URL + this.setValue( + DateType().apply { + value = + Date.from( + sevenDaysAgo.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant(), + ) + addExtension( Extension( EXTENSION_CQF_CALCULATED_VALUE_URL, Expression().apply { + expression = "today() - 3 'days'" language = "text/fhirpath" - expression = "yesterday()" // invalid FHIRPath expression }, ), ) - }, - ) - }, - ) - } - - val twoDaysAgo = - Date.from( - LocalDate.now().minusDays(2).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant(), - ) - val answer = QuestionnaireResponseItemAnswerComponent().apply { value = DateType(twoDaysAgo) } + }, + ) + }, + ) + } + val answer = + QuestionnaireResponse.QuestionnaireResponseItemAnswerComponent().apply { + value = + DateType().apply { + val fiveDaysAgo = LocalDate.now().minusDays(5) + value = + Date.from(fiveDaysAgo.atStartOfDay().atZone(ZoneId.systemDefault()).toInstant()) + } + } - val validationResult = - MinValueValidator.validate( - questionnaireItem, - answer, - InstrumentationRegistry.getInstrumentation().context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) - } + val validationResult = + MinValueValidator.validate(questionnaireItem, answer, context) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) + } - assertThat(validationResult.isValid).isTrue() - assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue() - } + assertThat(validationResult.isValid).isFalse() + assertThat(validationResult.errorMessage) + .isEqualTo("Minimum value allowed is:${LocalDate.now().minusDays(3)}") + } @Test - fun `should return valid result and removes constraint for an answer with an empty value`() { - val questionnaireItem = - Questionnaire.QuestionnaireItemComponent().apply { - addExtension( - Extension().apply { - url = MIN_VALUE_EXTENSION_URL - this.setValue( - DateType().apply { - extension = - listOf( - Extension( - EXTENSION_CQF_CALCULATED_VALUE_URL, - Expression().apply { - language = "text/fhirpath" - expression = "today()" - }, - ), - ) - }, - ) - }, + fun `should return valid result and removes constraint for an answer value when minValue cqf-calculatedValue evaluates to empty`() = + runTest { + val questionnaireItem = + Questionnaire.QuestionnaireItemComponent().apply { + addExtension( + Extension().apply { + url = MIN_VALUE_EXTENSION_URL + this.setValue( + DateType().apply { + extension = + listOf( + Extension( + EXTENSION_CQF_CALCULATED_VALUE_URL, + Expression().apply { + language = "text/fhirpath" + expression = "yesterday()" // invalid FHIRPath expression + }, + ), + ) + }, + ) + }, + ) + } + + val twoDaysAgo = + Date.from( + LocalDate.now().minusDays(2).atStartOfDay().atZone(ZoneId.systemDefault()).toInstant(), ) - } + val answer = QuestionnaireResponseItemAnswerComponent().apply { value = DateType(twoDaysAgo) } - val answer = QuestionnaireResponseItemAnswerComponent().apply { value = DateType() } + val validationResult = + MinValueValidator.validate( + questionnaireItem, + answer, + InstrumentationRegistry.getInstrumentation().context, + ) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) + } - val validationResult = - MinValueValidator.validate( - questionnaireItem, - answer, - InstrumentationRegistry.getInstrumentation().context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) - } + assertThat(validationResult.isValid).isTrue() + assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue() + } - assertThat(validationResult.isValid).isTrue() - assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue() - } + @Test + fun `should return valid result and removes constraint for an answer with an empty value`() = + runTest { + val questionnaireItem = + Questionnaire.QuestionnaireItemComponent().apply { + addExtension( + Extension().apply { + url = MIN_VALUE_EXTENSION_URL + this.setValue( + DateType().apply { + extension = + listOf( + Extension( + EXTENSION_CQF_CALCULATED_VALUE_URL, + Expression().apply { + language = "text/fhirpath" + expression = "today()" + }, + ), + ) + }, + ) + }, + ) + } + + val answer = QuestionnaireResponseItemAnswerComponent().apply { value = DateType() } + + val validationResult = + MinValueValidator.validate( + questionnaireItem, + answer, + InstrumentationRegistry.getInstrumentation().context, + ) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) + } + + assertThat(validationResult.isValid).isTrue() + assertThat(validationResult.errorMessage.isNullOrBlank()).isTrue() + } } diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidatorTest.kt index d9f98eefd5..a0219d3f27 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/QuestionnaireResponseItemValidatorTest.kt @@ -20,6 +20,7 @@ import android.content.Context import android.os.Build import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.Extension import org.hl7.fhir.r4.model.IntegerType import org.hl7.fhir.r4.model.Questionnaire @@ -43,7 +44,7 @@ class QuestionnaireResponseItemValidatorTest { } @Test - fun `should return valid result`() { + fun `should return valid result`() = runTest { val questionnaireItem = Questionnaire.QuestionnaireItemComponent().apply { addExtension( @@ -77,15 +78,15 @@ class QuestionnaireResponseItemValidatorTest { questionnaireItem, answers, context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + ) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) } assertThat(validationResult).isEqualTo(Valid) } @Test - fun `should validate individual answers and combine results`() { + fun `should validate individual answers and combine results`() = runTest { val questionnaireItem = Questionnaire.QuestionnaireItemComponent().apply { linkId = "a-question" @@ -121,8 +122,8 @@ class QuestionnaireResponseItemValidatorTest { questionnaireItem, answers, context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + ) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) } assertThat(validationResult).isInstanceOf(Invalid::class.java) @@ -132,7 +133,7 @@ class QuestionnaireResponseItemValidatorTest { } @Test - fun `should validate all answers`() { + fun `should validate all answers`() = runTest { val questionnaireItem = Questionnaire.QuestionnaireItemComponent().apply { type = Questionnaire.QuestionnaireItemType.INTEGER @@ -145,8 +146,8 @@ class QuestionnaireResponseItemValidatorTest { questionnaireItem, answers, context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + ) { + TestExpressionValueEvaluator.evaluate(questionnaireItem, it) } assertThat(validationResult).isInstanceOf(Invalid::class.java) diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/RegexValidatorTest.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/RegexValidatorTest.kt index 1613e9f0b3..6baf6f5b98 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/RegexValidatorTest.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/RegexValidatorTest.kt @@ -22,6 +22,7 @@ import androidx.test.core.app.ApplicationProvider import com.google.common.truth.Truth.assertThat import java.net.URI import java.text.SimpleDateFormat +import kotlinx.coroutines.test.runTest import org.hl7.fhir.r4.model.BooleanType import org.hl7.fhir.r4.model.DateType import org.hl7.fhir.r4.model.DecimalType @@ -52,37 +53,37 @@ class RegexValidatorTest { } @Test - fun boolean_notMatchingRegex_shouldReturnInvalidResult() { + fun boolean_notMatchingRegex_shouldReturnInvalidResult() = runTest { checkAnswerNotMatchingRegex(regex = "true", value = BooleanType(false)) } @Test - fun boolean_matchingRegex_shouldReturnValidResult() { + fun boolean_matchingRegex_shouldReturnValidResult() = runTest { checkAnswerMatchingRegex(regex = "true", value = BooleanType(true)) } @Test - fun decimal_notMatchingRegex_shouldReturnInvalidResult() { + fun decimal_notMatchingRegex_shouldReturnInvalidResult() = runTest { checkAnswerNotMatchingRegex(regex = "[0-9]+\\.[0-9]+", value = DecimalType(31234)) } @Test - fun decimal_matchingRegex_shouldReturnValidResult() { + fun decimal_matchingRegex_shouldReturnValidResult() = runTest { checkAnswerMatchingRegex(regex = "[0-9]+\\.[0-9]+", value = DecimalType(3.1415926535)) } @Test - fun int_notMatchingRegex_shouldReturnInvalidResult() { + fun int_notMatchingRegex_shouldReturnInvalidResult() = runTest { checkAnswerNotMatchingRegex(regex = "[0-9]+", value = IntegerType(-1)) } @Test - fun int_matchingRegex_shouldReturnValidResult() { + fun int_matchingRegex_shouldReturnValidResult() = runTest { checkAnswerMatchingRegex(regex = "[0-9]+", value = IntegerType(1234567890)) } @Test - fun dateType_notMatchingRegex_shouldReturnInvalidResult() { + fun dateType_notMatchingRegex_shouldReturnInvalidResult() = runTest { val dateFormat = SimpleDateFormat("yyyy-MM-dd") checkAnswerNotMatchingRegex( regex = "[0-9]{2}-[0-9]{2}-[0-9]{2}", @@ -91,7 +92,7 @@ class RegexValidatorTest { } @Test - fun date_matchingRegex_shouldReturnValidResult() { + fun date_matchingRegex_shouldReturnValidResult() = runTest { val dateFormat = SimpleDateFormat("yyyy-MM-dd") checkAnswerMatchingRegex( regex = "[0-9]{4}-[0-9]{2}-[0-9]{2}", @@ -100,17 +101,17 @@ class RegexValidatorTest { } @Test - fun time_matchingRegex_shouldReturnInvalidResult() { + fun time_matchingRegex_shouldReturnInvalidResult() = runTest { checkAnswerNotMatchingRegex(regex = "[0-9]{2}:[0-9]{2}", value = TimeType("18:00:59")) } @Test - fun time_notMatchingRegex_shouldReturnValidResult() { + fun time_notMatchingRegex_shouldReturnValidResult() = runTest { checkAnswerMatchingRegex(regex = "[0-9]{2}:[0-9]{2}:[0-9]{2}", value = TimeType("18:00:59")) } @Test - fun string_notMatchingRegex_shouldReturnInvalidResult() { + fun string_notMatchingRegex_shouldReturnInvalidResult() = runTest { checkAnswerNotMatchingRegex( regex = "^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]", value = StringType("www.hl7.org"), @@ -118,7 +119,7 @@ class RegexValidatorTest { } @Test - fun string_matchingRegex_shouldReturnValidResult() { + fun string_matchingRegex_shouldReturnValidResult() = runTest { checkAnswerMatchingRegex( regex = "^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]", value = StringType("https://www.hl7.org/"), @@ -126,7 +127,7 @@ class RegexValidatorTest { } @Test - fun uri_notMatchingRegex_shouldReturnInvalidResult() { + fun uri_notMatchingRegex_shouldReturnInvalidResult() = runTest { checkAnswerNotMatchingRegex( regex = "[a-z]+", value = UriType(URI.create("https://www.hl7.org/")), @@ -134,7 +135,7 @@ class RegexValidatorTest { } @Test - fun uri_matchingRegex_shouldReturnValidResult() { + fun uri_matchingRegex_shouldReturnValidResult() = runTest { checkAnswerMatchingRegex( regex = "^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]", value = UriType(URI.create("https://www.hl7.org/")), @@ -142,12 +143,12 @@ class RegexValidatorTest { } @Test - fun invalidRegex_shouldReturnValidResult() { + fun invalidRegex_shouldReturnValidResult() = runTest { checkAnswerMatchingRegex("[.*", StringType("http://www.google.com")) } @Test - fun nonPrimitive_notMatchingRegex_shouldReturnValidResult() { + fun nonPrimitive_notMatchingRegex_shouldReturnValidResult() = runTest { val requirement = Questionnaire.QuestionnaireItemComponent().apply { addExtension( @@ -161,8 +162,8 @@ class RegexValidatorTest { QuestionnaireResponseItemAnswerComponent().apply { this.value = Quantity(1234567.89) } val validationResult = - RegexValidator.validate(requirement, response, context) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + RegexValidator.validate(requirement, response, context) { + TestExpressionValueEvaluator.evaluate(requirement, it) } assertThat(validationResult.isValid).isTrue() @@ -174,7 +175,7 @@ class RegexValidatorTest { var context: Context = ApplicationProvider.getApplicationContext() @JvmStatic - fun checkAnswerMatchingRegex(regex: String, value: PrimitiveType<*>) { + suspend fun checkAnswerMatchingRegex(regex: String, value: PrimitiveType<*>) { val testComponent = createRegexQuestionnaireTestItem(regex, value) val validationResult = @@ -182,8 +183,8 @@ class RegexValidatorTest { testComponent.requirement, testComponent.answer, context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + ) { + TestExpressionValueEvaluator.evaluate(testComponent.requirement, it) } assertThat(validationResult.isValid).isTrue() @@ -191,7 +192,7 @@ class RegexValidatorTest { } @JvmStatic - fun checkAnswerNotMatchingRegex(regex: String, value: PrimitiveType<*>) { + suspend fun checkAnswerNotMatchingRegex(regex: String, value: PrimitiveType<*>) { val testComponent = createRegexQuestionnaireTestItem(regex, value) val validationResult = @@ -199,8 +200,8 @@ class RegexValidatorTest { testComponent.requirement, testComponent.answer, context, - ) { extension, expression -> - CalculatedValueExpressionEvaluator.evaluate(extension.value, expression) + ) { + TestExpressionValueEvaluator.evaluate(testComponent.requirement, it) } assertThat(validationResult.isValid).isFalse() diff --git a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/CalculatedValueExpressionEvaluator.kt b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/TestExpressionValueEvaluator.kt similarity index 83% rename from datacapture/src/test/java/com/google/android/fhir/datacapture/validation/CalculatedValueExpressionEvaluator.kt rename to datacapture/src/test/java/com/google/android/fhir/datacapture/validation/TestExpressionValueEvaluator.kt index b96020879b..5cb104fb75 100644 --- a/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/CalculatedValueExpressionEvaluator.kt +++ b/datacapture/src/test/java/com/google/android/fhir/datacapture/validation/TestExpressionValueEvaluator.kt @@ -17,17 +17,18 @@ package com.google.android.fhir.datacapture.validation import com.google.android.fhir.datacapture.fhirpath.evaluateToBase +import org.hl7.fhir.r4.model.Base import org.hl7.fhir.r4.model.Expression import org.hl7.fhir.r4.model.Type -object CalculatedValueExpressionEvaluator { +object TestExpressionValueEvaluator { /** * Doesn't handle expressions containing FHIRPath supplements * https://build.fhir.org/ig/HL7/sdc/expressions.html#fhirpath-supplements */ - fun evaluate(type: Type, expression: Expression): Type? = + fun evaluate(base: Base, expression: Expression): Type? = try { - evaluateToBase(type, expression.expression).singleOrNull() as? Type + evaluateToBase(base, expression.expression).singleOrNull() as? Type } catch (_: Exception) { null }