diff options
13 files changed, 896 insertions, 0 deletions
diff --git a/java/src/com/android/intentresolver/v2/validation/Findings.kt b/java/src/com/android/intentresolver/v2/validation/Findings.kt new file mode 100644 index 00000000..9a3cc9c7 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/Findings.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2.validation + +import android.util.Log +import com.android.intentresolver.v2.validation.Importance.CRITICAL +import com.android.intentresolver.v2.validation.Importance.WARNING +import kotlin.reflect.KClass + +sealed interface Finding { + val importance: Importance + val message: String +} + +enum class Importance { + CRITICAL, + WARNING, +} + +val Finding.logcatPriority + get() = + when (importance) { + CRITICAL -> Log.ERROR + else -> Log.WARN + } + +private fun formatMessage(key: String? = null, msg: String) = buildString { + key?.also { append("['$key']: ") } + append(msg) +} + +data class IgnoredValue( + val key: String, + val reason: String, +) : Finding { + override val importance = WARNING + + override val message: String + get() = formatMessage(key, "Ignored. $reason") +} + +data class RequiredValueMissing( + val key: String, + val allowedType: KClass<*>, +) : Finding { + + override val importance = CRITICAL + + override val message: String + get() = + formatMessage( + key, + "expected value of ${allowedType.simpleName}, " + "but no value was present" + ) +} + +data class WrongElementType( + val key: String, + override val importance: Importance, + val container: KClass<*>, + val actualType: KClass<*>, + val expectedType: KClass<*> +) : Finding { + override val message: String + get() = + formatMessage( + key, + "${container.simpleName} expected with elements of " + + "${expectedType.simpleName} " + + "but found ${actualType.simpleName} values instead" + ) +} + +data class ValueIsWrongType( + val key: String, + override val importance: Importance, + val actualType: KClass<*>, + val allowedTypes: List<KClass<*>>, +) : Finding { + + override val message: String + get() = + formatMessage( + key, + "expected value of ${allowedTypes.map(KClass<*>::simpleName)} " + + "but was ${actualType.simpleName}" + ) +} + +data class UncaughtException(val thrown: Throwable, val key: String? = null) : Finding { + override val importance: Importance + get() = CRITICAL + override val message: String + get() = + formatMessage( + key, + "An unhandled exception was caught during validation: " + + thrown.stackTraceToString() + ) +} diff --git a/java/src/com/android/intentresolver/v2/validation/Validation.kt b/java/src/com/android/intentresolver/v2/validation/Validation.kt new file mode 100644 index 00000000..46939602 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/Validation.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2.validation + +import com.android.intentresolver.v2.validation.Importance.CRITICAL +import com.android.intentresolver.v2.validation.Importance.WARNING + +/** + * Provides a mechanism for validating a result from a set of properties. + * + * The results of validation are provided as [findings]. + */ +interface Validation { + val findings: List<Finding> + + /** + * Require a valid property. + * + * If [property] is not valid, this [Validation] will be immediately completed as [Invalid]. + * + * @param property the required property + * @return a valid **T** + */ + @Throws(InvalidResultError::class) fun <T> required(property: Validator<T>): T + + /** + * Request an optional value for a property. + * + * If [property] is not valid, this [Validation] will be immediately completed as [Invalid]. + * + * @param property the required property + * @return a valid **T** + */ + fun <T> optional(property: Validator<T>): T? + + /** + * Report a property as __ignored__. + * + * The presence of any value will report a warning citing [reason]. + */ + fun <T> ignored(property: Validator<T>, reason: String) +} + +/** Performs validation for a specific key -> value pair. */ +interface Validator<T> { + val key: String + + /** + * Performs validation on a specific value from [source]. + * + * @param source a source for reading the property value. Values are intentionally untyped + * (Any?) to avoid upstream code from making type assertions through type inference. Types are + * asserted later using a [Validator]. + * @param importance the importance of any findings + */ + fun validate(source: (String) -> Any?, importance: Importance): ValidationResult<T> +} + +internal class InvalidResultError internal constructor() : Error() + +/** + * Perform a number of validations on the source, assembling and returning a Result. + * + * When an exception is thrown by [validate], it is caught here. In response, a failed + * [ValidationResult] is returned containing a [CRITICAL] [Finding] for the exception. + * + * @param validate perform validations and return a [ValidationResult] + */ +fun <T> validateFrom(source: (String) -> Any?, validate: Validation.() -> T): ValidationResult<T> { + val validation = ValidationImpl(source) + return runCatching { validate(validation) } + .fold( + onSuccess = { result -> Valid(result, validation.findings) }, + onFailure = { + when (it) { + // A validator has interrupted validation. Return the findings. + is InvalidResultError -> Invalid(validation.findings) + + // Some other exception was thrown from [validate], + else -> Invalid(findings = listOf(UncaughtException(it))) + } + } + ) +} + +private class ValidationImpl(val source: (String) -> Any?) : Validation { + override val findings = mutableListOf<Finding>() + + override fun <T> optional(property: Validator<T>): T? = validate(property, WARNING) + + override fun <T> required(property: Validator<T>): T { + return validate(property, CRITICAL) ?: throw InvalidResultError() + } + + override fun <T> ignored(property: Validator<T>, reason: String) { + val result = property.validate(source, WARNING) + if (result.value != null) { + // Note: Any findings about the value (result.findings) are ignored. + findings += IgnoredValue(property.key, reason) + } + } + + private fun <T> validate(property: Validator<T>, importance: Importance): T? { + return runCatching { property.validate(source, importance) } + .fold( + onSuccess = { result -> + findings += result.findings + result.value + }, + onFailure = { + findings += UncaughtException(it, property.key) + null + } + ) + } +} diff --git a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt new file mode 100644 index 00000000..092cabe8 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2.validation + +import android.util.Log + +sealed interface ValidationResult<T> { + val value: T? + val findings: List<Finding> + + fun isSuccess() = value != null + + fun getOrThrow(): T = + checkNotNull(value) { "The result was invalid: " + findings.joinToString(separator = "\n") } + + fun <T> reportToLogcat(tag: String) { + findings.forEach { Log.println(it.logcatPriority, tag, it.toString()) } + } +} + +data class Valid<T>(override val value: T?, override val findings: List<Finding> = emptyList()) : + ValidationResult<T> + +data class Invalid<T>(override val findings: List<Finding>) : ValidationResult<T> { + override val value: T? = null +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt new file mode 100644 index 00000000..3cefeb15 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2.validation.types + +import android.content.Intent +import android.net.Uri +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator +import com.android.intentresolver.v2.validation.ValueIsWrongType + +class IntentOrUri(override val key: String) : Validator<Intent> { + + override fun validate( + source: (String) -> Any?, + importance: Importance + ): ValidationResult<Intent> { + + return when (val value = source(key)) { + // An intent, return it. + is Intent -> Valid(value) + + // A Uri was supplied. + // Unfortunately, converting Uri -> Intent requires a toString(). + is Uri -> Valid(Intent.parseUri(value.toString(), Intent.URI_INTENT_SCHEME)) + + // No value present. + null -> createResult(importance, RequiredValueMissing(key, Intent::class)) + + // Some other type. + else -> { + return createResult( + importance, + ValueIsWrongType( + key, + importance, + actualType = value::class, + allowedTypes = listOf(Intent::class, Uri::class) + ) + ) + } + } + } +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt new file mode 100644 index 00000000..c6c4abba --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2.validation.types + +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator +import com.android.intentresolver.v2.validation.ValueIsWrongType +import com.android.intentresolver.v2.validation.WrongElementType +import kotlin.reflect.KClass +import kotlin.reflect.cast + +class ParceledArray<T : Any>( + override val key: String, + private val elementType: KClass<T>, +) : Validator<List<T>> { + + override fun validate( + source: (String) -> Any?, + importance: Importance + ): ValidationResult<List<T>> { + + return when (val value: Any? = source(key)) { + // No value present. + null -> createResult(importance, RequiredValueMissing(key, elementType)) + + // A parcel does not transfer the element type information for parcelable + // arrays. This leads to a restored type of Array<Parcelable>, which is + // incompatible with Array<T : Parcelable>. + + // To handle this safely, treat as Array<*>, assert contents of the expected + // parcelable type, and return as a list. + + is Array<*> -> { + val invalid = value.filterNotNull().firstOrNull { !elementType.isInstance(it) } + when (invalid) { + // No invalid elements, result is ok. + null -> Valid(value.map { elementType.cast(it) }) + + // At least one incorrect element type found. + else -> + createResult( + importance, + WrongElementType( + key, + importance, + actualType = invalid::class, + container = Array::class, + expectedType = elementType + ) + ) + } + } + + // The value is not an Array at all. + else -> + createResult( + importance, + ValueIsWrongType( + key, + importance, + actualType = value::class, + allowedTypes = listOf(elementType) + ) + ) + } + } +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt new file mode 100644 index 00000000..3287b84b --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2.validation.types + +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator +import com.android.intentresolver.v2.validation.ValueIsWrongType +import kotlin.reflect.KClass +import kotlin.reflect.cast + +class SimpleValue<T : Any>( + override val key: String, + private val expected: KClass<T>, +) : Validator<T> { + + override fun validate(source: (String) -> Any?, importance: Importance): ValidationResult<T> { + val value: Any? = source(key) + return when { + // The value is present and of the expected type. + expected.isInstance(value) -> return Valid(expected.cast(value)) + + // No value is present. + value == null -> createResult(importance, RequiredValueMissing(key, expected)) + + // The value is some other type. + else -> + createResult( + importance, + ValueIsWrongType( + key, + importance, + actualType = value::class, + allowedTypes = listOf(expected) + ) + ) + } + } +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/Validators.kt b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt new file mode 100644 index 00000000..4e6e5dff --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.v2.validation.types + +import com.android.intentresolver.v2.validation.Finding +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.Importance.CRITICAL +import com.android.intentresolver.v2.validation.Importance.WARNING +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator + +inline fun <reified T : Any> value(key: String): Validator<T> { + return SimpleValue(key, T::class) +} + +inline fun <reified T : Any> array(key: String): Validator<List<T>> { + return ParceledArray(key, T::class) +} + +/** + * Convenience function to wrap a finding in an appropriate result type. + * + * An error [finding] is suppressed when [importance] == [WARNING] + */ +internal fun <T> createResult(importance: Importance, finding: Finding): ValidationResult<T> { + return when (importance) { + WARNING -> Valid(null, listOf(finding).filter { it.importance == WARNING }) + CRITICAL -> Invalid(listOf(finding)) + } +} diff --git a/tests/shared/Android.bp b/tests/shared/Android.bp index dbd68b12..55188ee3 100644 --- a/tests/shared/Android.bp +++ b/tests/shared/Android.bp @@ -32,5 +32,6 @@ java_library { "hamcrest", "IntentResolver-core", "mockito-target-minus-junit4", + "truth" ], } diff --git a/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt b/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt new file mode 100644 index 00000000..1ff0ce8e --- /dev/null +++ b/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt @@ -0,0 +1,22 @@ +package com.android.intentresolver.v2.validation + +import com.google.common.truth.FailureMetadata +import com.google.common.truth.IterableSubject +import com.google.common.truth.Subject +import com.google.common.truth.Truth.assertAbout + +class ValidationResultSubject(metadata: FailureMetadata, private val actual: ValidationResult<*>?) : + Subject(metadata, actual) { + + fun isSuccess() = check("isSuccess()").that(actual?.isSuccess()).isTrue() + fun isFailure() = check("isSuccess()").that(actual?.isSuccess()).isFalse() + + fun value(): Subject = check("value").that(actual?.value) + + fun findings(): IterableSubject = check("findings").that(actual?.findings) + + companion object { + fun assertThat(input: ValidationResult<*>): ValidationResultSubject = + assertAbout(::ValidationResultSubject).that(input) + } +} diff --git a/tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt new file mode 100644 index 00000000..43fb448c --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt @@ -0,0 +1,99 @@ +package com.android.intentresolver.v2.validation + +import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat +import com.android.intentresolver.v2.validation.types.value +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.fail +import org.junit.Test + +class ValidationTest { + + /** Test required values. */ + @Test + fun required_valuePresent() { + val result: ValidationResult<String> = + validateFrom({ 1 }) { + val required: Int = required(value<Int>("key")) + "return value: $required" + } + assertThat(result).value().isEqualTo("return value: 1") + assertThat(result).findings().isEmpty() + } + + /** Test reporting of absent required values. */ + @Test + fun required_valueAbsent() { + val result: ValidationResult<String> = + validateFrom({ null }) { + required(value<Int>("key")) + fail("'required' should have thrown an exception") + "return value" + } + assertThat(result).isFailure() + assertThat(result).findings().containsExactly( + RequiredValueMissing("key", Int::class)) + } + + /** Test optional values are ignored when absent. */ + @Test + fun optional_valuePresent() { + val result: ValidationResult<String> = + validateFrom({ 1 }) { + val optional: Int? = optional(value<Int>("key")) + "return value: $optional" + } + assertThat(result).value().isEqualTo("return value: 1") + assertThat(result).findings().isEmpty() + } + + /** Test optional values are ignored when absent. */ + @Test + fun optional_valueAbsent() { + val result: ValidationResult<String?> = + validateFrom({ null }) { + val optional: String? = optional(value<String>("key")) + "return value: $optional" + } + assertThat(result).isSuccess() + assertThat(result).findings().isEmpty() + } + + /** Test reporting of ignored values. */ + @Test + fun ignored_valuePresent() { + val result: ValidationResult<String> = + validateFrom(mapOf("key" to 1)::get) { + ignored(value<Int>("key"), "no longer supported") + "result value" + } + assertThat(result).value().isEqualTo("result value") + assertThat(result) + .findings() + .containsExactly(IgnoredValue("key", "no longer supported")) + } + + /** Test reporting of ignored values. */ + @Test + fun ignored_valueAbsent() { + val result: ValidationResult<String> = + validateFrom({ null }) { + ignored(value<Int>("key"), "ignored when option foo is set") + "result value" + } + assertThat(result).value().isEqualTo("result value") + assertThat(result).findings().isEmpty() + } + + /** Test handling of exceptions in the validation function. */ + @Test + fun thrown_exception() { + val result: ValidationResult<String> = + validateFrom({ null }) { + error("something") + } + assertThat(result).isFailure() + val findingTypes = result.findings.map { it::class } + assertThat(findingTypes.first()).isEqualTo(UncaughtException::class) + } + +} diff --git a/tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt new file mode 100644 index 00000000..ad230488 --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt @@ -0,0 +1,107 @@ +package com.android.intentresolver.v2.validation.types + +import android.content.Intent +import android.content.Intent.URI_INTENT_SCHEME +import android.net.Uri +import androidx.core.net.toUri +import androidx.test.ext.truth.content.IntentSubject.assertThat +import com.android.intentresolver.v2.validation.Importance.CRITICAL +import com.android.intentresolver.v2.validation.Importance.WARNING +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat +import com.android.intentresolver.v2.validation.ValueIsWrongType +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class IntentOrUriTest { + + /** Test for validation success when the value is an Intent. */ + @Test + fun intent() { + val keyValidator = IntentOrUri("key") + val values = mapOf("key" to Intent("GO")) + + val result = keyValidator.validate(values::get, CRITICAL) + assertThat(result).findings().isEmpty() + assertThat(result.value).hasAction("GO") + } + + /** Test for validation success when the value is a Uri. */ + @Test + fun uri() { + val keyValidator = IntentOrUri("key") + val values = mapOf("key" to Intent("GO").toUri(URI_INTENT_SCHEME).toUri()) + + val result = keyValidator.validate(values::get, CRITICAL) + assertThat(result).findings().isEmpty() + assertThat(result.value).hasAction("GO") + } + + /** Test the failure result when the value is missing. */ + @Test + fun missing() { + val keyValidator = IntentOrUri("key") + + val result = keyValidator.validate({ null }, CRITICAL) + + assertThat(result).value().isNull() + assertThat(result).findings().containsExactly(RequiredValueMissing("key", Intent::class)) + } + + /** Check validation passes when value is null and importance is [WARNING] (optional). */ + @Test + fun optional() { + val keyValidator = ParceledArray("key", Intent::class) + + val result = keyValidator.validate(source = { null }, WARNING) + + assertThat(result).findings().isEmpty() + assertThat(result.value).isNull() + } + + /** + * Test for failure result when the value is neither Intent nor Uri, with importance CRITICAL. + */ + @Test + fun wrongType_required() { + val keyValidator = IntentOrUri("key") + val values = mapOf("key" to 1) + + val result = keyValidator.validate(values::get, CRITICAL) + + assertThat(result).value().isNull() + assertThat(result) + .findings() + .containsExactly( + ValueIsWrongType( + "key", + importance = CRITICAL, + actualType = Int::class, + allowedTypes = listOf(Intent::class, Uri::class) + ) + ) + } + + /** + * Test for warnings when the value is neither Intent nor Uri, with importance WARNING. + */ + @Test + fun wrongType_optional() { + val keyValidator = IntentOrUri("key") + val values = mapOf("key" to 1) + + val result = keyValidator.validate(values::get, WARNING) + + assertThat(result).value().isNull() + assertThat(result) + .findings() + .containsExactly( + ValueIsWrongType( + "key", + importance = WARNING, + actualType = Int::class, + allowedTypes = listOf(Intent::class, Uri::class) + ) + ) + } +} diff --git a/tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt new file mode 100644 index 00000000..d4dca01b --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt @@ -0,0 +1,93 @@ +package com.android.intentresolver.v2.validation.types + +import android.content.Intent +import android.graphics.Point +import com.android.intentresolver.v2.validation.Importance.CRITICAL +import com.android.intentresolver.v2.validation.Importance.WARNING +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat +import com.android.intentresolver.v2.validation.ValueIsWrongType +import com.android.intentresolver.v2.validation.WrongElementType +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class ParceledArrayTest { + + /** Check that a array is handled correctly when valid. */ + @Test + fun valid() { + val keyValidator = ParceledArray("key", elementType = String::class) + val values = mapOf("key" to arrayOf("String")) + + val result = keyValidator.validate(values::get, CRITICAL) + + assertThat(result).findings().isEmpty() + assertThat(result.value).containsExactly("String") + } + + /** Check correct failure result when an array has the wrong element type. */ + @Test + fun wrongElementType() { + val keyValidator = ParceledArray("key", elementType = Intent::class) + val values = mapOf("key" to arrayOf(Point())) + + val result = keyValidator.validate(values::get, CRITICAL) + + assertThat(result).value().isNull() + assertThat(result) + .findings() + .containsExactly( + // TODO: report with a new class `WrongElementType` to improve clarity + WrongElementType( + "key", + importance = CRITICAL, + container = Array::class, + actualType = Point::class, + expectedType = Intent::class + ) + ) + } + + /** Check correct failure result when an array value is missing. */ + @Test + fun missing() { + val keyValidator = ParceledArray("key", Intent::class) + + val result = keyValidator.validate(source = { null }, CRITICAL) + + assertThat(result).value().isNull() + assertThat(result).findings().containsExactly(RequiredValueMissing("key", Intent::class)) + } + + /** Check validation passes when value is null and importance is [WARNING] (optional). */ + @Test + fun optional() { + val keyValidator = ParceledArray("key", Intent::class) + + val result = keyValidator.validate(source = { null }, WARNING) + + assertThat(result).findings().isEmpty() + assertThat(result.value).isNull() + } + + /** Check correct failure result when the array value itself is the wrong type. */ + @Test + fun wrongType() { + val keyValidator = ParceledArray("key", Intent::class) + val values = mapOf("key" to 1) + + val result = keyValidator.validate(values::get, CRITICAL) + + assertThat(result).value().isNull() + assertThat(result) + .findings() + .containsExactly( + ValueIsWrongType( + "key", + importance = CRITICAL, + actualType = Int::class, + allowedTypes = listOf(Intent::class) + ) + ) + } +} diff --git a/tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt new file mode 100644 index 00000000..13bb4b33 --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt @@ -0,0 +1,52 @@ +package com.android.intentresolver.v2.validation.types + +import com.android.intentresolver.v2.validation.Importance.CRITICAL +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat +import com.android.intentresolver.v2.validation.ValueIsWrongType +import org.junit.Test + +class SimpleValueTest { + + /** Test for validation success when the value is present and the correct type. */ + @Test + fun present() { + val keyValidator = SimpleValue("key", expected = Double::class) + val values = mapOf("key" to Math.PI) + + val result = keyValidator.validate(values::get, CRITICAL) + assertThat(result).findings().isEmpty() + assertThat(result).value().isEqualTo(Math.PI) + } + + /** Test for validation success when the value is present and the correct type. */ + @Test + fun wrongType() { + val keyValidator = SimpleValue("key", expected = Double::class) + val values = mapOf("key" to "Apple Pie") + + val result = keyValidator.validate(values::get, CRITICAL) + assertThat(result).value().isNull() + assertThat(result) + .findings() + .containsExactly( + ValueIsWrongType( + "key", + importance = CRITICAL, + actualType = String::class, + allowedTypes = listOf(Double::class) + ) + ) + } + + /** Test the failure result when the value is missing. */ + @Test + fun missing() { + val keyValidator = SimpleValue("key", expected = Double::class) + + val result = keyValidator.validate(source = { null }, CRITICAL) + + assertThat(result).value().isNull() + assertThat(result).findings().containsExactly(RequiredValueMissing("key", Double::class)) + } +} |