Merge "Allow any actor when target package is debuggable"
This commit is contained in:
committed by
Android (Google) Code Review
commit
9e751fd4e7
@@ -26,6 +26,7 @@ import android.os.Process;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
import com.android.internal.util.ArrayUtils;
|
||||
import com.android.internal.util.CollectionUtils;
|
||||
|
||||
@@ -66,7 +67,7 @@ public class OverlayActorEnforcer {
|
||||
|
||||
String actorNamespace = actorUri.getAuthority();
|
||||
Map<String, String> namespace = namedActors.get(actorNamespace);
|
||||
if (namespace == null) {
|
||||
if (ArrayUtils.isEmpty(namespace)) {
|
||||
return Pair.create(null, ActorState.MISSING_NAMESPACE);
|
||||
}
|
||||
|
||||
@@ -102,21 +103,32 @@ public class OverlayActorEnforcer {
|
||||
* See {@link OverlayActorEnforcer} class comment for actor requirements.
|
||||
* @return true if the actor is allowed to act on the target overlayInfo
|
||||
*/
|
||||
private ActorState isAllowedActor(String methodName, OverlayInfo overlayInfo,
|
||||
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
|
||||
public ActorState isAllowedActor(String methodName, OverlayInfo overlayInfo,
|
||||
int callingUid, int userId) {
|
||||
// Checked first to avoid package not found errors, which are ignored for calls from shell
|
||||
switch (callingUid) {
|
||||
case Process.ROOT_UID:
|
||||
case Process.SYSTEM_UID:
|
||||
return ActorState.ALLOWED;
|
||||
}
|
||||
|
||||
final String targetPackageName = overlayInfo.targetPackageName;
|
||||
final PackageInfo targetPkgInfo = mPackageManager.getPackageInfo(targetPackageName, userId);
|
||||
if (targetPkgInfo == null) {
|
||||
return ActorState.TARGET_NOT_FOUND;
|
||||
}
|
||||
|
||||
if ((targetPkgInfo.applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
|
||||
return ActorState.ALLOWED;
|
||||
}
|
||||
|
||||
String[] callingPackageNames = mPackageManager.getPackagesForUid(callingUid);
|
||||
if (ArrayUtils.isEmpty(callingPackageNames)) {
|
||||
return ActorState.NO_PACKAGES_FOR_UID;
|
||||
}
|
||||
|
||||
// A target is always an allowed actor for itself
|
||||
String targetPackageName = overlayInfo.targetPackageName;
|
||||
if (ArrayUtils.contains(callingPackageNames, targetPackageName)) {
|
||||
return ActorState.ALLOWED;
|
||||
}
|
||||
@@ -149,7 +161,7 @@ public class OverlayActorEnforcer {
|
||||
targetOverlayable = mPackageManager.getOverlayableForTarget(targetPackageName,
|
||||
targetOverlayableName, userId);
|
||||
} catch (IOException e) {
|
||||
return ActorState.UNABLE_TO_GET_TARGET;
|
||||
return ActorState.UNABLE_TO_GET_TARGET_OVERLAYABLE;
|
||||
}
|
||||
|
||||
if (targetOverlayable == null) {
|
||||
@@ -189,7 +201,7 @@ public class OverlayActorEnforcer {
|
||||
}
|
||||
|
||||
// Currently only pre-installed apps can be actors
|
||||
if (!appInfo.isSystemApp() && !appInfo.isUpdatedSystemApp()) {
|
||||
if (!appInfo.isSystemApp()) {
|
||||
return ActorState.ACTOR_NOT_PREINSTALLED;
|
||||
}
|
||||
|
||||
@@ -203,22 +215,25 @@ public class OverlayActorEnforcer {
|
||||
/**
|
||||
* For easier logging/debugging, a set of all possible failure/success states when running
|
||||
* enforcement.
|
||||
*
|
||||
* The ordering of this enum should be maintained in the order that cases are checked in code,
|
||||
* as this ordering is used inside OverlayActorEnforcerTests.
|
||||
*/
|
||||
public enum ActorState {
|
||||
ALLOWED,
|
||||
INVALID_ACTOR,
|
||||
MISSING_NAMESPACE,
|
||||
MISSING_PACKAGE,
|
||||
MISSING_APP_INFO,
|
||||
ACTOR_NOT_PREINSTALLED,
|
||||
TARGET_NOT_FOUND,
|
||||
NO_PACKAGES_FOR_UID,
|
||||
MISSING_ACTOR_NAME,
|
||||
ERROR_READING_OVERLAYABLE,
|
||||
MISSING_TARGET_OVERLAYABLE_NAME,
|
||||
MISSING_LEGACY_PERMISSION,
|
||||
ERROR_READING_OVERLAYABLE,
|
||||
UNABLE_TO_GET_TARGET_OVERLAYABLE,
|
||||
MISSING_OVERLAYABLE,
|
||||
INVALID_OVERLAYABLE_ACTOR_NAME,
|
||||
NO_NAMED_ACTORS,
|
||||
UNABLE_TO_GET_TARGET,
|
||||
MISSING_LEGACY_PERMISSION
|
||||
MISSING_NAMESPACE,
|
||||
MISSING_ACTOR_NAME,
|
||||
MISSING_APP_INFO,
|
||||
ACTOR_NOT_PREINSTALLED,
|
||||
INVALID_ACTOR,
|
||||
ALLOWED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,172 +21,387 @@ import android.content.om.OverlayableInfo
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.os.Process
|
||||
import org.junit.Rule
|
||||
import com.android.server.om.OverlayActorEnforcer.ActorState
|
||||
import com.android.server.testutils.mockThrowOnUnmocked
|
||||
import com.android.server.testutils.whenever
|
||||
import com.google.common.truth.Truth.assertThat
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Test
|
||||
import org.junit.rules.ExpectedException
|
||||
import java.lang.UnsupportedOperationException
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
import org.mockito.Mockito.spy
|
||||
import java.io.IOException
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class OverlayActorEnforcerTests {
|
||||
|
||||
companion object {
|
||||
private const val NAMESPACE = "testnamespace"
|
||||
private const val ACTOR_NAME = "testactor"
|
||||
private const val ACTOR_PKG_NAME = "com.test.actor.one"
|
||||
private const val TARGET_PKG = "com.test.target"
|
||||
private const val OVERLAY_PKG = "com.test.overlay"
|
||||
|
||||
private const val VALID_NAMESPACE = "testNamespaceValid"
|
||||
private const val INVALID_NAMESPACE = "testNamespaceInvalid"
|
||||
private const val VALID_ACTOR_NAME = "testActorOne"
|
||||
private const val INVALID_ACTOR_NAME = "testActorTwo"
|
||||
private const val VALID_ACTOR_PKG = "com.test.actor.valid"
|
||||
private const val INVALID_ACTOR_PKG = "com.test.actor.invalid"
|
||||
private const val OVERLAYABLE_NAME = "TestOverlayable"
|
||||
private const val UID = 3536
|
||||
private const val NULL_UID = 3536
|
||||
private const val EMPTY_UID = NULL_UID + 1
|
||||
private const val INVALID_ACTOR_UID = NULL_UID + 2
|
||||
private const val VALID_ACTOR_UID = NULL_UID + 3
|
||||
private const val TARGET_UID = NULL_UID + 4
|
||||
private const val USER_ID = 55
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val expectedException = ExpectedException.none()!!
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun parameters() = CASES.mapIndexed { caseIndex, testCase ->
|
||||
fun param(pair: Pair<String, TestState.() -> Unit>, type: Params.Type): Params {
|
||||
val expectedState = testCase.state.takeUnless { type == Params.Type.ALLOWED }
|
||||
?: ActorState.ALLOWED
|
||||
val (caseName, case) = pair
|
||||
val testName = makeTestName(testCase, caseName, type)
|
||||
return Params(caseIndex, expectedState, testName, type, case)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isRoot() {
|
||||
verify(callingUid = Process.ROOT_UID)
|
||||
}
|
||||
testCase.failures.map { param(it, Params.Type.FAILURE) } +
|
||||
testCase.allowed.map { param(it, Params.Type.ALLOWED) }
|
||||
}.flatten()
|
||||
|
||||
@Test(expected = SecurityException::class)
|
||||
fun isShell() {
|
||||
verify(callingUid = Process.SHELL_UID)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun isSystem() {
|
||||
verify(callingUid = Process.SYSTEM_UID)
|
||||
}
|
||||
|
||||
@Test(expected = SecurityException::class)
|
||||
fun noOverlayable_noTarget() {
|
||||
verify(targetOverlayableName = null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun noOverlayable_noTarget_withPermission() {
|
||||
verify(targetOverlayableName = null, hasPermission = true)
|
||||
}
|
||||
|
||||
@Test(expected = SecurityException::class)
|
||||
fun noOverlayable_withTarget() {
|
||||
verify(targetOverlayableName = OVERLAYABLE_NAME)
|
||||
}
|
||||
|
||||
@Test(expected = SecurityException::class)
|
||||
fun withOverlayable_noTarget() {
|
||||
verify(
|
||||
targetOverlayableName = null,
|
||||
overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, null)
|
||||
)
|
||||
}
|
||||
|
||||
@Test(expected = SecurityException::class)
|
||||
fun withOverlayable_noActor() {
|
||||
verify(
|
||||
overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, null)
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun withOverlayable_noActor_withPermission() {
|
||||
verify(
|
||||
hasPermission = true,
|
||||
overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, null)
|
||||
)
|
||||
}
|
||||
|
||||
@Test(expected = SecurityException::class)
|
||||
fun withOverlayable_withActor_notActor() {
|
||||
verify(
|
||||
isActor = false,
|
||||
overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
|
||||
"overlay://$NAMESPACE/$ACTOR_NAME")
|
||||
)
|
||||
}
|
||||
|
||||
@Test(expected = SecurityException::class)
|
||||
fun withOverlayable_withActor_isActor_notPreInstalled() {
|
||||
verify(
|
||||
isActor = true,
|
||||
isPreInstalled = false,
|
||||
overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
|
||||
"overlay://$NAMESPACE/$ACTOR_NAME")
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun withOverlayable_withActor_isActor_isPreInstalled() {
|
||||
verify(
|
||||
isActor = true,
|
||||
isPreInstalled = true,
|
||||
overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
|
||||
"overlay://$NAMESPACE/$ACTOR_NAME")
|
||||
)
|
||||
}
|
||||
|
||||
@Test(expected = SecurityException::class)
|
||||
fun withOverlayable_invalidActor() {
|
||||
verify(
|
||||
isActor = true,
|
||||
isPreInstalled = true,
|
||||
overlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, "notValidActor")
|
||||
)
|
||||
}
|
||||
|
||||
private fun verify(
|
||||
isActor: Boolean = false,
|
||||
isPreInstalled: Boolean = false,
|
||||
hasPermission: Boolean = false,
|
||||
overlayableInfo: OverlayableInfo? = null,
|
||||
callingUid: Int = UID,
|
||||
targetOverlayableName: String? = OVERLAYABLE_NAME
|
||||
) {
|
||||
val callback = MockCallback(
|
||||
isActor = isActor,
|
||||
isPreInstalled = isPreInstalled,
|
||||
hasPermission = hasPermission,
|
||||
overlayableInfo = overlayableInfo
|
||||
)
|
||||
|
||||
val overlayInfo = overlayInfo(targetOverlayableName)
|
||||
OverlayActorEnforcer(callback)
|
||||
.enforceActor(overlayInfo, "test", callingUid, USER_ID)
|
||||
}
|
||||
|
||||
private fun overlayInfo(targetOverlayableName: String?) = OverlayInfo("com.test.overlay",
|
||||
"com.test.target", targetOverlayableName, null, "/path", OverlayInfo.STATE_UNKNOWN, 0,
|
||||
0, false)
|
||||
|
||||
private class MockCallback(
|
||||
private val isActor: Boolean = false,
|
||||
private val isPreInstalled: Boolean = false,
|
||||
private val hasPermission: Boolean = false,
|
||||
private val overlayableInfo: OverlayableInfo? = null,
|
||||
private vararg val packageNames: String = arrayOf("com.test.actor.one")
|
||||
) : PackageManagerHelper {
|
||||
|
||||
override fun getNamedActors() = if (isActor) {
|
||||
mapOf(NAMESPACE to mapOf(ACTOR_NAME to ACTOR_PKG_NAME))
|
||||
} else {
|
||||
emptyMap()
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun checkAllCasesHandled() {
|
||||
// Assert that all states have been tested at least once.
|
||||
assertThat(CASES.map { it.state }.distinct()).containsAllIn(ActorState.values())
|
||||
}
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun checkAllCasesUniquelyNamed() {
|
||||
val duplicateCaseNames = CASES.mapIndexed { caseIndex, testCase ->
|
||||
testCase.failures.map {
|
||||
makeTestName(testCase, it.first, Params.Type.FAILURE)
|
||||
} + testCase.allowed.map {
|
||||
makeTestName(testCase, it.first, Params.Type.ALLOWED)
|
||||
}
|
||||
}
|
||||
.flatten()
|
||||
.groupingBy { it }
|
||||
.eachCount()
|
||||
.filterValues { it > 1 }
|
||||
.keys
|
||||
|
||||
assertThat(duplicateCaseNames).isEmpty()
|
||||
}
|
||||
|
||||
/*
|
||||
The pattern in this block is a result of the incredible number of branches in
|
||||
enforcement logic. It serves to verify failures with the assumption that all errors
|
||||
are checked in order. The idea is to emulate the if-else branches from code, but using
|
||||
actual test data instead of if statements.
|
||||
|
||||
Each state is verified by providing a failure or exclusive set of failures which cause
|
||||
a failure state to be returned. Each state also provides a success case which will
|
||||
"skip" the state. This allows subsequent failure cases to cascade from the first case
|
||||
by calling all the skip branches for preceding states and then choosing only 1 of
|
||||
the failures to test.
|
||||
|
||||
Given the failure states A, B, and C: testA calls A.failure + assert, testB calls
|
||||
A.skip + B.failure + assert, testC calls A.skip + B.skip + C.failure + assert, etc.
|
||||
|
||||
Calling `allowed` is a special case for when there is a combination of parameters that
|
||||
skips the remaining checks and immediately allows the actor through. For these cases,
|
||||
the first failure branch will be run, assert that it's not allowed, and the
|
||||
allowed branch will run, asserting that it now results in ALLOWED, skipping all
|
||||
remaining functions.
|
||||
|
||||
This is an ordered list of TestCase objects, with the possibility to repeat failure
|
||||
states if any can occur multiple times in the logic tree.
|
||||
|
||||
Each failure must be handled at least once.
|
||||
*/
|
||||
private val CASES = listOf(
|
||||
ActorState.TARGET_NOT_FOUND withCases {
|
||||
failure("nullPkgInfo") { targetPkgInfo = null }
|
||||
allowed("debuggable") {
|
||||
targetPkgInfo = pkgInfo(TARGET_PKG).apply {
|
||||
applicationInfo.flags = ApplicationInfo.FLAG_DEBUGGABLE
|
||||
}
|
||||
}
|
||||
skip { targetPkgInfo = pkgInfo(TARGET_PKG) }
|
||||
},
|
||||
ActorState.NO_PACKAGES_FOR_UID withCases {
|
||||
failure("empty") { callingUid = EMPTY_UID }
|
||||
failure("null") { callingUid = NULL_UID }
|
||||
failure("shell") { callingUid = Process.SHELL_UID }
|
||||
allowed("targetUid") { callingUid = TARGET_UID }
|
||||
allowed("rootUid") { callingUid = Process.ROOT_UID }
|
||||
allowed("systemUid") { callingUid = Process.SYSTEM_UID }
|
||||
skip { callingUid = INVALID_ACTOR_UID }
|
||||
},
|
||||
ActorState.MISSING_TARGET_OVERLAYABLE_NAME withCases {
|
||||
failure("nullTargetOverlayableName") {
|
||||
overlayInfoParams.targetOverlayableName = null
|
||||
targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
|
||||
"overlay://$VALID_NAMESPACE/$VALID_ACTOR_NAME")
|
||||
}
|
||||
skip { overlayInfoParams.targetOverlayableName = OVERLAYABLE_NAME }
|
||||
},
|
||||
ActorState.MISSING_LEGACY_PERMISSION withCases {
|
||||
failure("noPermission") {
|
||||
overlayInfoParams.targetOverlayableName = null
|
||||
targetOverlayableInfo = null
|
||||
hasPermission = false
|
||||
}
|
||||
allowed("hasPermission") { hasPermission = true }
|
||||
skip { overlayInfoParams.targetOverlayableName = OVERLAYABLE_NAME }
|
||||
},
|
||||
ActorState.ERROR_READING_OVERLAYABLE withCases {
|
||||
failure("doesTargetDefineOverlayableIOException") {
|
||||
overlayInfoParams.targetOverlayableName = null
|
||||
whenever(doesTargetDefineOverlayable(TARGET_PKG, USER_ID))
|
||||
.thenThrow(IOException::class.java)
|
||||
}
|
||||
skip { overlayInfoParams.targetOverlayableName = OVERLAYABLE_NAME }
|
||||
},
|
||||
ActorState.UNABLE_TO_GET_TARGET_OVERLAYABLE withCases {
|
||||
failure("getOverlayableForTargetIOException") {
|
||||
whenever(getOverlayableForTarget(TARGET_PKG, OVERLAYABLE_NAME,
|
||||
USER_ID)).thenThrow(IOException::class.java)
|
||||
}
|
||||
},
|
||||
ActorState.MISSING_OVERLAYABLE withCases {
|
||||
failure("nullTargetOverlayableInfo") { targetOverlayableInfo = null }
|
||||
skip {
|
||||
targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
|
||||
"overlay://$VALID_NAMESPACE/$VALID_ACTOR_NAME")
|
||||
}
|
||||
},
|
||||
ActorState.MISSING_LEGACY_PERMISSION withCases {
|
||||
failure("noPermissionNullActor") {
|
||||
targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, null)
|
||||
hasPermission = false
|
||||
}
|
||||
failure("noPermissionEmptyActor") {
|
||||
targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, "")
|
||||
hasPermission = false
|
||||
}
|
||||
allowed("hasPermissionNullActor") {
|
||||
hasPermission = true
|
||||
}
|
||||
skip {
|
||||
targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME,
|
||||
"overlay://$VALID_NAMESPACE/$VALID_ACTOR_NAME")
|
||||
}
|
||||
},
|
||||
ActorState.INVALID_OVERLAYABLE_ACTOR_NAME withCases {
|
||||
fun TestState.mockActor(actorUri: String) {
|
||||
targetOverlayableInfo = OverlayableInfo(OVERLAYABLE_NAME, actorUri)
|
||||
}
|
||||
failure("wrongScheme") {
|
||||
mockActor("notoverlay://$VALID_NAMESPACE/$VALID_ACTOR_NAME")
|
||||
}
|
||||
failure("extraPath") {
|
||||
mockActor("overlay://$VALID_NAMESPACE/$VALID_ACTOR_NAME/extraPath")
|
||||
}
|
||||
failure("missingPath") { mockActor("overlay://$VALID_NAMESPACE") }
|
||||
failure("missingAuthority") { mockActor("overlay://") }
|
||||
skip { mockActor("overlay://$VALID_NAMESPACE/$VALID_ACTOR_NAME") }
|
||||
},
|
||||
ActorState.NO_NAMED_ACTORS withCases {
|
||||
failure("empty") { namedActorsMap = emptyMap() }
|
||||
skip {
|
||||
namedActorsMap = mapOf(INVALID_NAMESPACE to
|
||||
mapOf(INVALID_ACTOR_NAME to VALID_ACTOR_PKG))
|
||||
}
|
||||
},
|
||||
ActorState.MISSING_NAMESPACE withCases {
|
||||
failure("invalidNamespace") {
|
||||
namedActorsMap = mapOf(INVALID_NAMESPACE to
|
||||
mapOf(INVALID_ACTOR_NAME to VALID_ACTOR_PKG))
|
||||
}
|
||||
skip {
|
||||
namedActorsMap = mapOf(VALID_NAMESPACE to
|
||||
mapOf(INVALID_ACTOR_NAME to VALID_ACTOR_PKG))
|
||||
}
|
||||
},
|
||||
ActorState.MISSING_ACTOR_NAME withCases {
|
||||
failure("invalidActorName") {
|
||||
namedActorsMap = mapOf(VALID_NAMESPACE to
|
||||
mapOf(INVALID_ACTOR_NAME to VALID_ACTOR_PKG))
|
||||
}
|
||||
skip {
|
||||
namedActorsMap = mapOf(VALID_NAMESPACE to
|
||||
mapOf(VALID_ACTOR_NAME to VALID_ACTOR_PKG))
|
||||
}
|
||||
},
|
||||
ActorState.MISSING_APP_INFO withCases {
|
||||
failure("nullActorPkgInfo") { actorPkgInfo = null }
|
||||
failure("nullActorAppInfo") {
|
||||
actorPkgInfo = PackageInfo().apply { applicationInfo = null }
|
||||
}
|
||||
skip { actorPkgInfo = pkgInfo(VALID_ACTOR_PKG) }
|
||||
},
|
||||
ActorState.ACTOR_NOT_PREINSTALLED withCases {
|
||||
failure("notSystem") {
|
||||
actorPkgInfo = pkgInfo(VALID_ACTOR_PKG).apply {
|
||||
applicationInfo.flags = 0
|
||||
}
|
||||
}
|
||||
skip {
|
||||
actorPkgInfo = pkgInfo(VALID_ACTOR_PKG).apply {
|
||||
applicationInfo.flags = ApplicationInfo.FLAG_SYSTEM
|
||||
}
|
||||
}
|
||||
},
|
||||
ActorState.INVALID_ACTOR withCases {
|
||||
failure("invalidUid") { callingUid = INVALID_ACTOR_UID }
|
||||
skip { callingUid = VALID_ACTOR_UID }
|
||||
},
|
||||
ActorState.ALLOWED withCases {
|
||||
// No point making an exception for this case in all of the test code, so
|
||||
// just pretend this is a failure that results in a success result code.
|
||||
failure("allowed") { /* Do nothing */ }
|
||||
}
|
||||
)
|
||||
|
||||
data class OverlayInfoParams(
|
||||
var targetPackageName: String = TARGET_PKG,
|
||||
var targetOverlayableName: String? = null
|
||||
) {
|
||||
fun toOverlayInfo() = OverlayInfo(
|
||||
OVERLAY_PKG,
|
||||
targetPackageName,
|
||||
targetOverlayableName,
|
||||
null,
|
||||
"/path",
|
||||
OverlayInfo.STATE_UNKNOWN, 0,
|
||||
0, false)
|
||||
}
|
||||
|
||||
private infix fun ActorState.withCases(block: TestCase.() -> Unit) =
|
||||
TestCase(this).apply(block)
|
||||
|
||||
private fun pkgInfo(pkgName: String): PackageInfo = mockThrowOnUnmocked {
|
||||
this.packageName = pkgName
|
||||
this.applicationInfo = ApplicationInfo().apply {
|
||||
this.packageName = pkgName
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeTestName(testCase: TestCase, caseName: String, type: Params.Type): String {
|
||||
val resultSuffix = if (type == Params.Type.ALLOWED) "allowed" else "failed"
|
||||
return "${testCase.state}_${resultSuffix}_$caseName"
|
||||
}
|
||||
}
|
||||
|
||||
@Parameterized.Parameter(0)
|
||||
lateinit var params: Params
|
||||
|
||||
@Test
|
||||
fun verify() {
|
||||
// Apply all the skip states before the failure to be verified
|
||||
val testState = CASES.take(params.index)
|
||||
.fold(TestState.create()) { testState, case ->
|
||||
testState.apply(case.skip)
|
||||
}
|
||||
|
||||
// If testing an allowed branch, first apply a failure to ensure it fails
|
||||
if (params.type == Params.Type.ALLOWED) {
|
||||
CASES[params.index].failures.firstOrNull()?.second?.run(testState::apply)
|
||||
assertThat(testState.toResult()).isNotEqualTo(ActorState.ALLOWED)
|
||||
}
|
||||
|
||||
// Apply the test case in the params to the collected state
|
||||
testState.apply(params.function)
|
||||
|
||||
// Assert the result matches the expected state
|
||||
assertThat(testState.toResult()).isEqualTo(params.expectedState)
|
||||
}
|
||||
|
||||
private fun TestState.toResult() = OverlayActorEnforcer(this)
|
||||
.isAllowedActor("test", overlayInfoParams.toOverlayInfo(), callingUid, USER_ID)
|
||||
|
||||
data class Params(
|
||||
var index: Int,
|
||||
var expectedState: ActorState,
|
||||
val testName: String,
|
||||
val type: Type,
|
||||
val function: TestState.() -> Unit
|
||||
) {
|
||||
override fun toString() = testName
|
||||
|
||||
enum class Type {
|
||||
FAILURE,
|
||||
ALLOWED
|
||||
}
|
||||
}
|
||||
|
||||
data class TestCase(
|
||||
val state: ActorState,
|
||||
val failures: MutableList<Pair<String, TestState.() -> Unit>> = mutableListOf(),
|
||||
var allowed: MutableList<Pair<String, TestState.() -> Unit>> = mutableListOf(),
|
||||
var skip: (TestState.() -> Unit) = {}
|
||||
) {
|
||||
fun failure(caseName: String, block: TestState.() -> Unit) {
|
||||
failures.add(caseName to block)
|
||||
}
|
||||
|
||||
fun allowed(caseName: String, block: TestState.() -> Unit) {
|
||||
allowed.add(caseName to block)
|
||||
}
|
||||
|
||||
fun skip(block: TestState.() -> Unit) {
|
||||
this.skip = block
|
||||
}
|
||||
}
|
||||
|
||||
open class TestState private constructor(
|
||||
var callingUid: Int = NULL_UID,
|
||||
val overlayInfoParams: OverlayInfoParams = OverlayInfoParams(),
|
||||
var namedActorsMap: Map<String, Map<String, String>> = emptyMap(),
|
||||
var hasPermission: Boolean = false,
|
||||
var targetOverlayableInfo: OverlayableInfo? = null,
|
||||
var targetPkgInfo: PackageInfo? = null,
|
||||
var actorPkgInfo: PackageInfo? = null,
|
||||
vararg val packageNames: String = arrayOf("com.test.actor.one")
|
||||
) : PackageManagerHelper {
|
||||
|
||||
companion object {
|
||||
// Enforce that new instances are spied
|
||||
fun create() = spy(TestState())!!
|
||||
}
|
||||
|
||||
override fun getNamedActors() = namedActorsMap
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun getOverlayableForTarget(
|
||||
packageName: String,
|
||||
targetOverlayableName: String,
|
||||
userId: Int
|
||||
) = overlayableInfo
|
||||
) = targetOverlayableInfo?.takeIf {
|
||||
// Protect against this method being called with the wrong package name
|
||||
targetPkgInfo == null || targetPkgInfo?.packageName == packageName
|
||||
}
|
||||
|
||||
override fun getPackagesForUid(uid: Int) = when (uid) {
|
||||
UID -> packageNames
|
||||
EMPTY_UID -> emptyArray()
|
||||
INVALID_ACTOR_UID -> arrayOf(INVALID_ACTOR_PKG)
|
||||
VALID_ACTOR_UID -> arrayOf(VALID_ACTOR_PKG)
|
||||
TARGET_UID -> arrayOf(TARGET_PKG)
|
||||
NULL_UID -> null
|
||||
else -> null
|
||||
}
|
||||
|
||||
override fun getPackageInfo(packageName: String, userId: Int) = PackageInfo().apply {
|
||||
applicationInfo = ApplicationInfo().apply {
|
||||
flags = if (isPreInstalled) ApplicationInfo.FLAG_SYSTEM else 0
|
||||
}
|
||||
}
|
||||
override fun getPackageInfo(packageName: String, userId: Int) =
|
||||
listOfNotNull(targetPkgInfo, actorPkgInfo).find { it.packageName == packageName }
|
||||
|
||||
@Throws(IOException::class) // Mockito requires this checked exception to be declared
|
||||
override fun doesTargetDefineOverlayable(targetPackageName: String?, userId: Int): Boolean {
|
||||
return overlayableInfo != null
|
||||
return targetOverlayableInfo?.takeIf {
|
||||
// Protect against this method being called with the wrong package name
|
||||
targetPkgInfo == null || targetPkgInfo?.packageName == targetPackageName
|
||||
} != null
|
||||
}
|
||||
|
||||
override fun enforcePermission(permission: String?, message: String?) {
|
||||
|
||||
@@ -62,7 +62,7 @@ fun <Type : Any?> whenever(mock: Type, block: InvocationOnMock.() -> Any?) =
|
||||
|
||||
fun whenever(mock: Unit) = Mockito.`when`(mock).thenAnswer { }
|
||||
|
||||
inline fun <reified T> spyThrowOnUnmocked(value: T?, block: T.() -> Unit): T {
|
||||
inline fun <reified T> spyThrowOnUnmocked(value: T?, block: T.() -> Unit = {}): T {
|
||||
val swappingAnswer = object : Answer<Any?> {
|
||||
var delegate: Answer<*> = Answers.RETURNS_DEFAULTS
|
||||
|
||||
@@ -79,4 +79,5 @@ inline fun <reified T> spyThrowOnUnmocked(value: T?, block: T.() -> Unit): T {
|
||||
}
|
||||
}
|
||||
|
||||
inline fun <reified T> mockThrowOnUnmocked(block: T.() -> Unit) = spyThrowOnUnmocked<T>(null, block)
|
||||
inline fun <reified T> mockThrowOnUnmocked(block: T.() -> Unit = {}) =
|
||||
spyThrowOnUnmocked<T>(null, block)
|
||||
|
||||
Reference in New Issue
Block a user