blob: 5cb3dfbfb8e984640167316c548961dd659813ce [file] [log] [blame]
/*
* Copyright (C) 2020 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 android.cts.instantapp.resolver
import android.app.InstantAppResolverService
import android.app.InstantAppResolverService.InstantAppResolutionCallback
import android.content.Intent
import android.content.pm.InstantAppRequestInfo
import android.net.Uri
import android.os.Bundle
import android.os.IRemoteCallback
import android.os.UserHandle
import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.rules.ExpectedException
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
import org.mockito.Answers
import org.mockito.Mock
import org.mockito.Mockito.doNothing
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyNoMoreInteractions
import org.mockito.MockitoAnnotations
import java.util.UUID
import kotlin.random.Random
private typealias Method = InstantAppResolverService.(InstantAppRequestInfo) -> Unit
@Suppress("max-line-length")
@RunWith(Parameterized::class)
class ResolverServiceMethodFallbackTest @Suppress("UNUSED_PARAMETER") constructor(
private val version: Int,
private val methodList: List<Method>,
private val info: InstantAppRequestInfo,
// Remaining only used to print human-readable test name
name: String,
isWebIntent: Boolean
) {
companion object {
// Since the resolution callback class is final, mock the IRemoteCallback and have it throw
// a unique exception to indicate it was called.
class TestRemoteCallbackException : Exception()
private val testIntentWeb = Intent(Intent.ACTION_VIEW,
Uri.parse("https://${this::class.java.canonicalName}.com"))
private val testIntentNotWeb = Intent(Intent.ACTION_VIEW,
Uri.parse("content://${this::class.java.canonicalName}"))
private val testRemoteCallback = object : IRemoteCallback {
override fun sendResult(data: Bundle?) = throw TestRemoteCallbackException()
override fun asBinder() = throw UnsupportedOperationException()
}
private val testResolutionCallback = InstantAppResolutionCallback(0, testRemoteCallback)
private val testArray = IntArray(10) { Random.nextInt() }
private val testToken = UUID.randomUUID().toString()
private val testUser = UserHandle(Integer.MAX_VALUE)
private val testInfoWeb = InstantAppRequestInfo(testIntentWeb, testArray, testUser,
false, testToken)
private val testInfoNotWeb = InstantAppRequestInfo(testIntentNotWeb, testArray, testUser,
false, testToken)
// Each section defines methods versions with later definitions falling back to
// earlier definitions. Each block receives an [InstantAppResolverService] and invokes
// the appropriate version with the test data defined above.
private val infoOne: Method = { onGetInstantAppResolveInfo(testArray, testToken,
testResolutionCallback) }
private val infoTwo: Method = { onGetInstantAppResolveInfo(it.intent, testArray, testToken,
testResolutionCallback) }
private val infoThree: Method = { onGetInstantAppResolveInfo(it.intent, testArray, testUser,
testToken, testResolutionCallback) }
private val infoFour: Method = { onGetInstantAppResolveInfo(it, testResolutionCallback) }
private val filterOne: Method = { onGetInstantAppIntentFilter(testArray, testToken,
testResolutionCallback) }
private val filterTwo: Method = { onGetInstantAppIntentFilter(it.intent, testArray,
testToken, testResolutionCallback) }
private val filterThree: Method = { onGetInstantAppIntentFilter(it.intent, testArray,
testUser, testToken, testResolutionCallback) }
private val filterFour: Method = { onGetInstantAppIntentFilter(it, testResolutionCallback) }
private val infoList = listOf(infoOne, infoTwo, infoThree, infoFour)
private val filterList = listOf(filterOne, filterTwo, filterThree, filterFour)
@JvmStatic
@Parameterized.Parameters(name = "{3} version {0}, isWeb = {4}")
fun parameters(): Array<Array<*>> {
// Sanity check that web intent logic hasn't changed
assertThat(testInfoWeb.intent.isWebIntent).isTrue()
assertThat(testInfoNotWeb.intent.isWebIntent).isFalse()
// Declare all the possible params
val versions = Array(5) { it }
val methods = arrayOf("ResolveInfo" to infoList, "IntentFilter" to filterList)
val infos = arrayOf(testInfoWeb, testInfoNotWeb)
// FlatMap params into every possible combination
return infos.flatMap { info ->
methods.flatMap { (name, methods) ->
versions.map { version ->
arrayOf(version, methods, info, name, info.intent.isWebIntent)
}
}
}.toTypedArray()
}
}
@field:Mock(answer = Answers.CALLS_REAL_METHODS)
lateinit var mockService: InstantAppResolverService
@get:Rule
val expectedException = ExpectedException.none()
@Before
fun setUpMocks() {
MockitoAnnotations.initMocks(this)
}
@Test
fun onGetInstantApp() {
if (version == 0) {
// No version of the API was implemented, so expect terminal case
if (info.intent.isWebIntent) {
// If web intent, terminal is total failure
expectedException.expect(IllegalStateException::class.java)
} else {
// Otherwise, terminal is a fail safe by calling [testRemoteCallback]
expectedException.expect(TestRemoteCallbackException::class.java)
}
} else if (version < 2 && !info.intent.isWebIntent) {
// Starting from v2, if resolving a non-web intent and a v2+ method isn't implemented,
// it fails safely by calling [testRemoteCallback]
expectedException.expect(TestRemoteCallbackException::class.java)
}
// Version 1 is the first method (index 0)
val methodIndex = version - 1
// Implement a method if necessary
methodList.getOrNull(methodIndex)?.invoke(doNothing().`when`(mockService), info)
// Call the latest API
methodList.last().invoke(mockService, info)
// Check all methods before implemented method are never called
(0 until methodIndex).forEach {
methodList[it].invoke(verify(mockService, never()), info)
}
// Check all methods from implemented method are called
(methodIndex until methodList.size).forEach {
methodList[it].invoke(verify(mockService), info)
}
verifyNoMoreInteractions(mockService)
}
}