diff --git a/services/core/java/com/android/server/om/OverlayActorEnforcer.java b/services/core/java/com/android/server/om/OverlayActorEnforcer.java index 8c03c6ce3092b..8121a43e90601 100644 --- a/services/core/java/com/android/server/om/OverlayActorEnforcer.java +++ b/services/core/java/com/android/server/om/OverlayActorEnforcer.java @@ -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 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 } } diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt index 08392737350af..154d42ce3f8d5 100644 --- a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt +++ b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt @@ -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 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 Unit>> = mutableListOf(), + var allowed: MutableList 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> = 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?) { diff --git a/services/tests/servicestests/utils-mockito/com/android/server/testutils/MockitoUtils.kt b/services/tests/servicestests/utils-mockito/com/android/server/testutils/MockitoUtils.kt index 30db3db677d93..59cbd1ce1d7b7 100644 --- a/services/tests/servicestests/utils-mockito/com/android/server/testutils/MockitoUtils.kt +++ b/services/tests/servicestests/utils-mockito/com/android/server/testutils/MockitoUtils.kt @@ -62,7 +62,7 @@ fun whenever(mock: Type, block: InvocationOnMock.() -> Any?) = fun whenever(mock: Unit) = Mockito.`when`(mock).thenAnswer { } -inline fun spyThrowOnUnmocked(value: T?, block: T.() -> Unit): T { +inline fun spyThrowOnUnmocked(value: T?, block: T.() -> Unit = {}): T { val swappingAnswer = object : Answer { var delegate: Answer<*> = Answers.RETURNS_DEFAULTS @@ -79,4 +79,5 @@ inline fun spyThrowOnUnmocked(value: T?, block: T.() -> Unit): T { } } -inline fun mockThrowOnUnmocked(block: T.() -> Unit) = spyThrowOnUnmocked(null, block) +inline fun mockThrowOnUnmocked(block: T.() -> Unit = {}) = + spyThrowOnUnmocked(null, block)