Add base classes for common ConnectivityService tests.

This sets up what is necessary for an instrumented
ConnectivityService to run. Users of this class are
meant to inherit CSTest.

This is still relatively basic and does not have all the
instrumentation in ConnectivityServiceTest. Developers
looking to extend CSTest may find some instrumentation
missingĀ ; when they add the missing instrumentation,
they should consider whether it should be generic for all
CSTests (and put it in base/), or whether it's local to
their own test suite. This should enable faster testing
as each CSTest children will only need to set up the
instrumentation it actually needs.

This patch also migrates a basic test to have a first user.

Bug: 272685721
Test: ConnectivityServiceTest
      CSBasicMethodsTest
Change-Id: I1c47f616af90629c9cb2a6ae89d992b19863e704
diff --git a/tests/integration/util/com/android/server/NetworkAgentWrapper.java b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
index 28edcb2..edd201d 100644
--- a/tests/integration/util/com/android/server/NetworkAgentWrapper.java
+++ b/tests/integration/util/com/android/server/NetworkAgentWrapper.java
@@ -64,6 +64,9 @@
 import java.util.function.Consumer;
 
 public class NetworkAgentWrapper implements TestableNetworkCallback.HasNetwork {
+    // Note : Please do not add any new instrumentation here. If you need new instrumentation,
+    // please add it in CSAgentWrapper and use subclasses of CSTest instead of adding more
+    // tools in ConnectivityServiceTest.
     private final NetworkCapabilities mNetworkCapabilities;
     private final HandlerThread mHandlerThread;
     private final Context mContext;
@@ -468,4 +471,8 @@
     public boolean isBypassableVpn() {
         return mNetworkAgentConfig.isBypassableVpn();
     }
+
+    // Note : Please do not add any new instrumentation here. If you need new instrumentation,
+    // please add it in CSAgentWrapper and use subclasses of CSTest instead of adding more
+    // tools in ConnectivityServiceTest.
 }
diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
index e5dec56..3243033 100755
--- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java
@@ -75,10 +75,7 @@
 import static android.net.ConnectivityManager.PROFILE_NETWORK_PREFERENCE_ENTERPRISE_NO_FALLBACK;
 import static android.net.ConnectivityManager.TYPE_ETHERNET;
 import static android.net.ConnectivityManager.TYPE_MOBILE;
-import static android.net.ConnectivityManager.TYPE_MOBILE_FOTA;
-import static android.net.ConnectivityManager.TYPE_MOBILE_MMS;
 import static android.net.ConnectivityManager.TYPE_MOBILE_SUPL;
-import static android.net.ConnectivityManager.TYPE_PROXY;
 import static android.net.ConnectivityManager.TYPE_VPN;
 import static android.net.ConnectivityManager.TYPE_WIFI;
 import static android.net.ConnectivitySettingsManager.PRIVATE_DNS_MODE_OFF;
@@ -492,6 +489,8 @@
  * Build, install and run with:
  *  runtest frameworks-net -c com.android.server.ConnectivityServiceTest
  */
+// TODO : move methods from this test to smaller tests in the 'connectivityservice' directory
+// to enable faster testing of smaller groups of functionality.
 @RunWith(DevSdkIgnoreRunner.class)
 @SmallTest
 @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)
@@ -1016,6 +1015,9 @@
     }
 
     private class TestNetworkAgentWrapper extends NetworkAgentWrapper {
+        // Note : Please do not add any new instrumentation here. If you need new instrumentation,
+        // please add it in CSAgentWrapper and use subclasses of CSTest instead of adding more
+        // tools in ConnectivityServiceTest.
         private static final int VALIDATION_RESULT_INVALID = 0;
 
         private static final long DATA_STALL_TIMESTAMP = 10L;
@@ -1340,6 +1342,9 @@
      * operations have been processed and test for them.
      */
     private static class MockNetworkFactory extends NetworkFactory {
+        // Note : Please do not add any new instrumentation here. If you need new instrumentation,
+        // please add it in CSTest and use subclasses of CSTest instead of adding more
+        // tools in ConnectivityServiceTest.
         private final AtomicBoolean mNetworkStarted = new AtomicBoolean(false);
 
         static class RequestEntry {
@@ -1476,6 +1481,10 @@
     }
 
     private class MockVpn extends Vpn implements TestableNetworkCallback.HasNetwork {
+        // Note : Please do not add any new instrumentation here. If you need new instrumentation,
+        // please add it in CSTest and use subclasses of CSTest instead of adding more
+        // tools in ConnectivityServiceTest.
+
         // Careful ! This is different from mNetworkAgent, because MockNetworkAgent does
         // not inherit from NetworkAgent.
         private TestNetworkAgentWrapper mMockNetworkAgent;
@@ -1852,6 +1861,9 @@
 
         MockitoAnnotations.initMocks(this);
 
+        // Note : Please do not add any new instrumentation here. If you need new instrumentation,
+        // please add it in CSTest and use subclasses of CSTest instead of adding more
+        // tools in ConnectivityServiceTest.
         doReturn(asList(PRIMARY_USER_INFO)).when(mUserManager).getAliveUsers();
         doReturn(asList(PRIMARY_USER_HANDLE)).when(mUserManager).getUserHandles(anyBoolean());
         doReturn(PRIMARY_USER_INFO).when(mUserManager).getUserInfo(PRIMARY_USER);
@@ -1938,6 +1950,9 @@
         setCaptivePortalMode(ConnectivitySettingsManager.CAPTIVE_PORTAL_MODE_PROMPT);
         setAlwaysOnNetworks(false);
         setPrivateDnsSettings(PRIVATE_DNS_MODE_OFF, "ignored.example.com");
+        // Note : Please do not add any new instrumentation here. If you need new instrumentation,
+        // please add it in CSTest and use subclasses of CSTest instead of adding more
+        // tools in ConnectivityServiceTest.
     }
 
     private void initMockedResources() {
@@ -1974,6 +1989,9 @@
         final ConnectivityResources mConnRes;
         final ArraySet<Pair<Long, Integer>> mEnabledChangeIds = new ArraySet<>();
 
+        // Note : Please do not add any new instrumentation here. If you need new instrumentation,
+        // please add it in CSTest and use subclasses of CSTest instead of adding more
+        // tools in ConnectivityServiceTest.
         ConnectivityServiceDependencies(final Context mockResContext) {
             mConnRes = new ConnectivityResources(mockResContext);
         }
@@ -2583,23 +2601,6 @@
     }
 
     @Test
-    public void testNetworkTypes() {
-        // Ensure that our mocks for the networkAttributes config variable work as expected. If they
-        // don't, then tests that depend on CONNECTIVITY_ACTION broadcasts for these network types
-        // will fail. Failing here is much easier to debug.
-        assertTrue(mCm.isNetworkSupported(TYPE_WIFI));
-        assertTrue(mCm.isNetworkSupported(TYPE_MOBILE));
-        assertTrue(mCm.isNetworkSupported(TYPE_MOBILE_MMS));
-        assertTrue(mCm.isNetworkSupported(TYPE_MOBILE_FOTA));
-        assertFalse(mCm.isNetworkSupported(TYPE_PROXY));
-
-        // Check that TYPE_ETHERNET is supported. Unlike the asserts above, which only validate our
-        // mocks, this assert exercises the ConnectivityService code path that ensures that
-        // TYPE_ETHERNET is supported if the ethernet service is running.
-        assertTrue(mCm.isNetworkSupported(TYPE_ETHERNET));
-    }
-
-    @Test
     public void testNetworkFeature() throws Exception {
         // Connect the cell agent and wait for the connected broadcast.
         mCellAgent = new TestNetworkAgentWrapper(TRANSPORT_CELLULAR);
@@ -18801,4 +18802,7 @@
 
         verifyClatdStop(null /* inOrder */, MOBILE_IFNAME);
     }
+
+    // Note : adding tests is ConnectivityServiceTest is deprecated, as it is too big for
+    // maintenance. Please consider adding new tests in subclasses of CSTest instead.
 }
diff --git a/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
new file mode 100644
index 0000000..6f8ba6c
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/CSBasicMethodsTest.kt
@@ -0,0 +1,35 @@
+@file:Suppress("DEPRECATION") // This file tests a bunch of deprecated methods : don't warn about it
+
+package com.android.server
+
+import android.net.ConnectivityManager
+import android.os.Build
+import androidx.test.filters.SmallTest
+import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo
+import com.android.testutils.DevSdkIgnoreRunner
+import org.junit.Test
+import org.junit.runner.RunWith
+import kotlin.test.assertFalse
+import kotlin.test.assertTrue
+
+@RunWith(DevSdkIgnoreRunner::class)
+@SmallTest
+@IgnoreUpTo(Build.VERSION_CODES.R)
+class CSBasicMethodsTest : CSTest() {
+    @Test
+    fun testNetworkTypes() {
+        // Ensure that mocks for the networkAttributes config variable work as expected. If they
+        // don't, then tests that depend on CONNECTIVITY_ACTION broadcasts for these network types
+        // will fail. Failing here is much easier to debug.
+        assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_WIFI))
+        assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE))
+        assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE_MMS))
+        assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_MOBILE_FOTA))
+        assertFalse(cm.isNetworkSupported(ConnectivityManager.TYPE_PROXY))
+
+        // Check that TYPE_ETHERNET is supported. Unlike the asserts above, which only validate our
+        // mocks, this assert exercises the ConnectivityService code path that ensures that
+        // TYPE_ETHERNET is supported if the ethernet service is running.
+        assertTrue(cm.isNetworkSupported(ConnectivityManager.TYPE_ETHERNET))
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
new file mode 100644
index 0000000..5ae9232
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSAgentWrapper.kt
@@ -0,0 +1,134 @@
+package com.android.server
+
+import android.content.Context
+import android.net.ConnectivityManager
+import android.net.INetworkMonitor
+import android.net.INetworkMonitorCallbacks
+import android.net.LinkProperties
+import android.net.Network
+import android.net.NetworkAgent
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
+import android.net.NetworkProvider
+import android.net.NetworkRequest
+import android.net.NetworkScore
+import android.net.NetworkTestResultParcelable
+import android.net.networkstack.NetworkStackClientBase
+import android.os.HandlerThread
+import com.android.modules.utils.build.SdkLevel
+import com.android.testutils.RecorderCallback.CallbackEntry.Available
+import com.android.testutils.TestableNetworkCallback
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doNothing
+import org.mockito.Mockito.verify
+import org.mockito.stubbing.Answer
+import java.util.concurrent.atomic.AtomicInteger
+import kotlin.test.assertEquals
+import kotlin.test.fail
+
+private inline fun <reified T> ArgumentCaptor() = ArgumentCaptor.forClass(T::class.java)
+
+private val agentCounter = AtomicInteger(1)
+private fun nextAgentId() = agentCounter.getAndIncrement()
+
+/**
+ * A wrapper for network agents, for use with CSTest.
+ *
+ * This class knows how to interact with CSTest and has helpful methods to make fake agents
+ * that can be manipulated directly from a test.
+ */
+class CSAgentWrapper(
+        val context: Context,
+        csHandlerThread: HandlerThread,
+        networkStack: NetworkStackClientBase,
+        nac: NetworkAgentConfig,
+        val nc: NetworkCapabilities,
+        val lp: LinkProperties,
+        val score: FromS<NetworkScore>,
+        val provider: NetworkProvider?
+) : TestableNetworkCallback.HasNetwork {
+    private val TAG = "CSAgent${nextAgentId()}"
+    private val VALIDATION_RESULT_INVALID = 0
+    private val VALIDATION_TIMESTAMP = 1234L
+    private val agent: NetworkAgent
+    private val nmCallbacks: INetworkMonitorCallbacks
+    val networkMonitor = mock<INetworkMonitor>()
+
+    override val network: Network get() = agent.network!!
+
+    init {
+        // Capture network monitor callbacks and simulate network monitor
+        val validateAnswer = Answer {
+            CSTest.CSTestExecutor.execute { onValidationRequested() }
+            null
+        }
+        doAnswer(validateAnswer).`when`(networkMonitor).notifyNetworkConnected(any(), any())
+        doAnswer(validateAnswer).`when`(networkMonitor).notifyNetworkConnectedParcel(any())
+        doAnswer(validateAnswer).`when`(networkMonitor).forceReevaluation(anyInt())
+        val nmNetworkCaptor = ArgumentCaptor<Network>()
+        val nmCbCaptor = ArgumentCaptor<INetworkMonitorCallbacks>()
+        doNothing().`when`(networkStack).makeNetworkMonitor(
+                nmNetworkCaptor.capture(),
+                any() /* name */,
+                nmCbCaptor.capture())
+
+        // Create the actual agent. NetworkAgent is abstract, so make an anonymous subclass.
+        if (SdkLevel.isAtLeastS()) {
+            agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
+                    nc, lp, score.value, nac, provider) {}
+        } else {
+            agent = object : NetworkAgent(context, csHandlerThread.looper, TAG,
+                    nc, lp, 50 /* score */, nac, provider) {}
+        }
+        agent.register()
+        assertEquals(agent.network!!.netId, nmNetworkCaptor.value.netId)
+        nmCallbacks = nmCbCaptor.value
+        nmCallbacks.onNetworkMonitorCreated(networkMonitor)
+    }
+
+    private fun onValidationRequested() {
+        if (SdkLevel.isAtLeastT()) {
+            verify(networkMonitor).notifyNetworkConnectedParcel(any())
+        } else {
+            verify(networkMonitor).notifyNetworkConnected(any(), any())
+        }
+        nmCallbacks.notifyProbeStatusChanged(0 /* completed */, 0 /* succeeded */)
+        val p = NetworkTestResultParcelable()
+        p.result = VALIDATION_RESULT_INVALID
+        p.probesAttempted = 0
+        p.probesSucceeded = 0
+        p.redirectUrl = null
+        p.timestampMillis = VALIDATION_TIMESTAMP
+        nmCallbacks.notifyNetworkTestedWithExtras(p)
+    }
+
+    fun connect() {
+        val mgr = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+        val request = NetworkRequest.Builder().clearCapabilities()
+                .addTransportType(nc.transportTypes[0])
+                .build()
+        val cb = TestableNetworkCallback()
+        mgr.registerNetworkCallback(request, cb)
+        agent.markConnected()
+        if (null == cb.poll { it is Available && agent.network == it.network }) {
+            if (!nc.hasCapability(NET_CAPABILITY_NOT_SUSPENDED) &&
+                    nc.hasTransport(TRANSPORT_CELLULAR)) {
+                // ConnectivityService adds NOT_SUSPENDED by default to all non-cell agents. An
+                // agent without NOT_SUSPENDED will not connect, instead going into the SUSPENDED
+                // state, so this call will not terminate.
+                // Instead of forcefully adding NOT_SUSPENDED to all agents like older tools did,
+                // it's better to let the developer manage it as they see fit but help them
+                // debug if they forget.
+                fail("Could not connect the agent. Did you forget to add " +
+                        "NET_CAPABILITY_NOT_SUSPENDED ?")
+            }
+            fail("Could not connect the agent. Instrumentation failure ?")
+        }
+        mgr.unregisterNetworkCallback(cb)
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
new file mode 100644
index 0000000..68613a6
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTest.kt
@@ -0,0 +1,239 @@
+package com.android.server
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.content.pm.UserInfo
+import android.content.res.Resources
+import android.net.ConnectivityManager
+import android.net.INetd
+import android.net.InetAddresses
+import android.net.IpPrefix
+import android.net.LinkAddress
+import android.net.LinkProperties
+import android.net.NetworkAgentConfig
+import android.net.NetworkCapabilities
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED
+import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED
+import android.net.NetworkPolicyManager
+import android.net.NetworkProvider
+import android.net.NetworkScore
+import android.net.PacProxyManager
+import android.net.RouteInfo
+import android.net.networkstack.NetworkStackClientBase
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.UserHandle
+import android.os.UserManager
+import android.telephony.TelephonyManager
+import android.testing.TestableContext
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.internal.util.test.BroadcastInterceptingContext
+import com.android.modules.utils.build.SdkLevel
+import com.android.networkstack.apishim.common.UnsupportedApiLevelException
+import com.android.server.connectivity.AutomaticOnOffKeepaliveTracker
+import com.android.server.connectivity.CarrierPrivilegeAuthenticator
+import com.android.server.connectivity.ClatCoordinator
+import com.android.server.connectivity.ConnectivityFlags
+import com.android.server.connectivity.MultinetworkPolicyTracker
+import com.android.server.connectivity.MultinetworkPolicyTrackerTestDependencies
+import com.android.server.connectivity.ProxyTracker
+import com.android.testutils.waitForIdle
+import org.mockito.AdditionalAnswers.delegatesTo
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.mock
+import java.util.concurrent.Executors
+import kotlin.test.fail
+
+internal const val HANDLER_TIMEOUT_MS = 2_000
+internal const val TEST_PACKAGE_NAME = "com.android.test.package"
+internal const val WIFI_WOL_IFNAME = "test_wlan_wol"
+internal val LOCAL_IPV4_ADDRESS = InetAddresses.parseNumericAddress("192.0.2.1")
+
+open class FromS<Type>(val value: Type)
+
+/**
+ * Base class for tests testing ConnectivityService and its satellites.
+ *
+ * This class sets up a ConnectivityService running locally in the test.
+ */
+// TODO (b/272685721) : make ConnectivityServiceTest smaller and faster by moving the setup
+// parts into this class and moving the individual tests to multiple separate classes.
+open class CSTest {
+    companion object {
+        val CSTestExecutor = Executors.newSingleThreadExecutor()
+    }
+
+    init {
+        if (!SdkLevel.isAtLeastS()) {
+            throw UnsupportedApiLevelException("CSTest subclasses must be annotated to only " +
+                    "run on S+, e.g. @DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R)");
+        }
+    }
+
+    val instrumentationContext =
+            TestableContext(InstrumentationRegistry.getInstrumentation().context)
+    val context = CSContext(instrumentationContext)
+
+    // See constructor for default-enabled features. All queried features must be either enabled
+    // or disabled, because the test can't hold READ_DEVICE_CONFIG and device config utils query
+    // permissions using static contexts.
+    val enabledFeatures = HashMap<String, Boolean>().also {
+        it[ConnectivityFlags.NO_REMATCH_ALL_REQUESTS_ON_REGISTER] = true
+        it[ConnectivityService.KEY_DESTROY_FROZEN_SOCKETS_VERSION] = true
+        it[ConnectivityService.DELAY_DESTROY_FROZEN_SOCKETS_VERSION] = true
+    }
+    fun enableFeature(f: String) = enabledFeatures.set(f, true)
+    fun disableFeature(f: String) = enabledFeatures.set(f, false)
+
+    // When adding new members, consider if it's not better to build the object in CSTestHelpers
+    // to keep this file clean of implementation details. Generally, CSTestHelpers should only
+    // need changes when new details of instrumentation are needed.
+    val contentResolver = makeMockContentResolver(context)
+
+    val PRIMARY_USER = 0
+    val PRIMARY_USER_INFO = UserInfo(PRIMARY_USER, "" /* name */, UserInfo.FLAG_PRIMARY)
+    val PRIMARY_USER_HANDLE = UserHandle(PRIMARY_USER)
+    val userManager = makeMockUserManager(PRIMARY_USER_INFO, PRIMARY_USER_HANDLE)
+    val activityManager = makeActivityManager()
+
+    val networkStack = mock<NetworkStackClientBase>()
+    val csHandlerThread = HandlerThread("CSTestHandler")
+    val sysResources = mock<Resources>().also { initMockedResources(it) }
+    val packageManager = makeMockPackageManager()
+    val connResources = makeMockConnResources(sysResources, packageManager)
+
+    val bpfNetMaps = mock<BpfNetMaps>()
+    val clatCoordinator = mock<ClatCoordinator>()
+    val proxyTracker = ProxyTracker(context, mock<Handler>(), 16 /* EVENT_PROXY_HAS_CHANGED */)
+    val alarmManager = makeMockAlarmManager()
+    val systemConfigManager = makeMockSystemConfigManager()
+    val telephonyManager = mock<TelephonyManager>().also {
+        doReturn(true).`when`(it).isDataCapable()
+    }
+
+    val deps = CSDeps()
+    val service = makeConnectivityService(context, deps).also { it.systemReadyInternal() }
+    val cm = ConnectivityManager(context, service)
+    val csHandler = Handler(csHandlerThread.looper)
+
+    inner class CSDeps : ConnectivityService.Dependencies() {
+        override fun getResources(ctx: Context) = connResources
+        override fun getBpfNetMaps(context: Context, netd: INetd) = this@CSTest.bpfNetMaps
+        override fun getClatCoordinator(netd: INetd?) = this@CSTest.clatCoordinator
+        override fun getNetworkStack() = this@CSTest.networkStack
+
+        override fun makeHandlerThread() = csHandlerThread
+        override fun makeProxyTracker(context: Context, connServiceHandler: Handler) = proxyTracker
+
+        override fun makeCarrierPrivilegeAuthenticator(context: Context, tm: TelephonyManager) =
+                if (SdkLevel.isAtLeastT()) mock<CarrierPrivilegeAuthenticator>() else null
+
+        private inner class AOOKTDeps(c: Context) : AutomaticOnOffKeepaliveTracker.Dependencies(c) {
+            override fun isTetheringFeatureNotChickenedOut(name: String): Boolean {
+                return isFeatureEnabled(context, name)
+            }
+        }
+        override fun makeAutomaticOnOffKeepaliveTracker(c: Context, h: Handler) =
+                AutomaticOnOffKeepaliveTracker(c, h, AOOKTDeps(c))
+
+        override fun makeMultinetworkPolicyTracker(c: Context, h: Handler, r: Runnable) =
+                MultinetworkPolicyTracker(c, h, r,
+                        MultinetworkPolicyTrackerTestDependencies(connResources.get()))
+
+        // All queried features must be mocked, because the test cannot hold the
+        // READ_DEVICE_CONFIG permission and device config utils use static methods for
+        // checking permissions.
+        override fun isFeatureEnabled(context: Context?, name: String?) =
+                enabledFeatures[name] ?: fail("Unmocked feature $name, see CSTest.enabledFeatures")
+    }
+
+    inner class CSContext(base: Context) : BroadcastInterceptingContext(base) {
+        val pacProxyManager = mock<PacProxyManager>()
+        val networkPolicyManager = mock<NetworkPolicyManager>()
+
+        override fun getPackageManager() = this@CSTest.packageManager
+        override fun getContentResolver() = this@CSTest.contentResolver
+
+        // TODO : buff up the capabilities of this permission scheme to allow checking for
+        // permission rejections
+        override fun checkPermission(permission: String, pid: Int, uid: Int) = PERMISSION_GRANTED
+        override fun checkCallingOrSelfPermission(permission: String) = PERMISSION_GRANTED
+
+        // Necessary for MultinetworkPolicyTracker, which tries to register a receiver for
+        // all users. The test can't do that since it doesn't hold INTERACT_ACROSS_USERS.
+        // TODO : ensure MultinetworkPolicyTracker's BroadcastReceiver is tested ; ideally,
+        // just returning null should not have tests pass
+        override fun registerReceiverForAllUsers(
+                receiver: BroadcastReceiver?,
+                filter: IntentFilter,
+                broadcastPermission: String?,
+                scheduler: Handler?
+        ): Intent? = null
+
+        // Create and cache user managers on the fly as necessary.
+        val userManagers = HashMap<UserHandle, UserManager>()
+        override fun createContextAsUser(user: UserHandle, flags: Int): Context {
+            val asUser = mock(Context::class.java, delegatesTo<Any>(this))
+            doReturn(user).`when`(asUser).getUser()
+            doAnswer { userManagers.computeIfAbsent(user) {
+                mock(UserManager::class.java, delegatesTo<Any>(userManager)) }
+            }.`when`(asUser).getSystemService(Context.USER_SERVICE)
+            return asUser
+        }
+
+        // List of mocked services. Add additional services here or in subclasses.
+        override fun getSystemService(serviceName: String) = when (serviceName) {
+            Context.CONNECTIVITY_SERVICE -> cm
+            Context.PAC_PROXY_SERVICE -> pacProxyManager
+            Context.NETWORK_POLICY_SERVICE -> networkPolicyManager
+            Context.ALARM_SERVICE -> alarmManager
+            Context.USER_SERVICE -> userManager
+            Context.ACTIVITY_SERVICE -> activityManager
+            Context.SYSTEM_CONFIG_SERVICE -> systemConfigManager
+            Context.TELEPHONY_SERVICE -> telephonyManager
+            Context.STATS_MANAGER -> null // Stats manager is final and can't be mocked
+            else -> super.getSystemService(serviceName)
+        }
+    }
+
+    // Utility methods for subclasses to use
+    fun waitForIdle() = csHandlerThread.waitForIdle(HANDLER_TIMEOUT_MS)
+
+    private fun emptyAgentConfig() = NetworkAgentConfig.Builder().build()
+    private fun defaultNc() = NetworkCapabilities.Builder()
+            // Add sensible defaults for agents that don't want to care
+            .addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+            .addCapability(NET_CAPABILITY_NOT_ROAMING)
+            .addCapability(NET_CAPABILITY_NOT_VCN_MANAGED)
+            .build()
+    private fun defaultScore() = FromS<NetworkScore>(NetworkScore.Builder().build())
+    private fun defaultLp() = LinkProperties().apply {
+        addLinkAddress(LinkAddress(LOCAL_IPV4_ADDRESS, 32))
+        addRoute(RouteInfo(IpPrefix("0.0.0.0/0"), null, null))
+    }
+
+    // Network agents. See CSAgentWrapper. This class contains utility methods to simplify
+    // creation.
+    fun Agent(
+            nac: NetworkAgentConfig = emptyAgentConfig(),
+            nc: NetworkCapabilities = defaultNc(),
+            lp: LinkProperties = defaultLp(),
+            score: FromS<NetworkScore> = defaultScore(),
+            provider: NetworkProvider? = null
+    ) = CSAgentWrapper(context, csHandlerThread, networkStack, nac, nc, lp, score, provider)
+
+    fun Agent(vararg transports: Int, lp: LinkProperties = defaultLp()): CSAgentWrapper {
+        val nc = NetworkCapabilities.Builder().apply {
+            transports.forEach {
+                addTransportType(it)
+            }
+        }.addCapability(NET_CAPABILITY_NOT_SUSPENDED)
+                .build()
+        return Agent(nc = nc, lp = lp)
+    }
+}
diff --git a/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
new file mode 100644
index 0000000..b8f2151
--- /dev/null
+++ b/tests/unit/java/com/android/server/connectivityservice/base/CSTestHelpers.kt
@@ -0,0 +1,140 @@
+@file:JvmName("CsTestHelpers")
+
+package com.android.server
+
+import android.app.ActivityManager
+import android.app.AlarmManager
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.FEATURE_BLUETOOTH
+import android.content.pm.PackageManager.FEATURE_ETHERNET
+import android.content.pm.PackageManager.FEATURE_WIFI
+import android.content.pm.PackageManager.FEATURE_WIFI_DIRECT
+import android.content.pm.UserInfo
+import android.content.res.Resources
+import android.net.IDnsResolver
+import android.net.INetd
+import android.net.metrics.IpConnectivityLog
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.SystemClock
+import android.os.SystemConfigManager
+import android.os.UserHandle
+import android.os.UserManager
+import android.provider.Settings
+import android.test.mock.MockContentResolver
+import com.android.connectivity.resources.R
+import com.android.internal.util.WakeupMessage
+import com.android.internal.util.test.FakeSettingsProvider
+import com.android.modules.utils.build.SdkLevel
+import com.android.server.ConnectivityService.Dependencies
+import com.android.server.connectivity.ConnectivityResources
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyLong
+import org.mockito.ArgumentMatchers.anyString
+import org.mockito.ArgumentMatchers.argThat
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.doNothing
+import kotlin.test.fail
+
+internal inline fun <reified T> mock() = Mockito.mock(T::class.java)
+internal inline fun <reified T> any() = any(T::class.java)
+
+internal fun makeMockContentResolver(context: Context) = MockContentResolver(context).apply {
+    addProvider(Settings.AUTHORITY, FakeSettingsProvider())
+}
+
+internal fun makeMockUserManager(info: UserInfo, handle: UserHandle) = mock<UserManager>().also {
+    doReturn(listOf(info)).`when`(it).getAliveUsers()
+    doReturn(listOf(handle)).`when`(it).getUserHandles(ArgumentMatchers.anyBoolean())
+}
+
+internal fun makeActivityManager() = mock<ActivityManager>().also {
+    if (SdkLevel.isAtLeastU()) {
+        doNothing().`when`(it).registerUidFrozenStateChangedCallback(any(), any())
+    }
+}
+
+internal fun makeMockPackageManager() = mock<PackageManager>().also { pm ->
+    val supported = listOf(FEATURE_WIFI, FEATURE_WIFI_DIRECT, FEATURE_BLUETOOTH, FEATURE_ETHERNET)
+    doReturn(true).`when`(pm).hasSystemFeature(argThat { supported.contains(it) })
+}
+
+internal fun makeMockConnResources(resources: Resources, pm: PackageManager) = mock<Context>().let {
+    doReturn(resources).`when`(it).resources
+    doReturn(pm).`when`(it).packageManager
+    ConnectivityResources.setResourcesContextForTest(it)
+    ConnectivityResources(it)
+}
+
+private val UNREASONABLY_LONG_ALARM_WAIT_MS = 1000
+internal fun makeMockAlarmManager() = mock<AlarmManager>().also { am ->
+    val alrmHdlr = HandlerThread("TestAlarmManager").also { it.start() }.threadHandler
+    doAnswer {
+        val (_, date, _, wakeupMsg, handler) = it.arguments
+        wakeupMsg as WakeupMessage
+        handler as Handler
+        val delayMs = ((date as Long) - SystemClock.elapsedRealtime()).coerceAtLeast(0)
+        if (delayMs > UNREASONABLY_LONG_ALARM_WAIT_MS) {
+            fail("Attempting to send msg more than $UNREASONABLY_LONG_ALARM_WAIT_MS" +
+                    "ms into the future : $delayMs")
+        }
+        alrmHdlr.postDelayed({ handler.post(wakeupMsg::onAlarm) }, wakeupMsg, delayMs)
+    }.`when`(am).setExact(eq(AlarmManager.ELAPSED_REALTIME_WAKEUP), anyLong(), anyString(),
+            any<WakeupMessage>(), any())
+    doAnswer {
+        alrmHdlr.removeCallbacksAndMessages(it.getArgument<WakeupMessage>(0))
+    }.`when`(am).cancel(any<WakeupMessage>())
+}
+
+internal fun makeMockSystemConfigManager() = mock<SystemConfigManager>().also {
+    doReturn(intArrayOf(0)).`when`(it).getSystemPermissionUids(anyString())
+}
+
+// Mocking resources used by ConnectivityService. Note these can't be defined to return the
+// value returned by the mocking, because a non-null method would mean the helper would also
+// return non-null and the compiler would check that, but mockito has no qualms returning null
+// from a @NonNull method when stubbing. Hence, mock() = doReturn().getString() would crash
+// at runtime, because getString() returns non-null String, therefore mock returns non-null String,
+// and kotlinc adds an intrinsics check for that, which crashes at runtime when mockito actually
+// returns null.
+private fun Resources.mock(r: Int, v: Boolean) { doReturn(v).`when`(this).getBoolean(r) }
+private fun Resources.mock(r: Int, v: Int) { doReturn(v).`when`(this).getInteger(r) }
+private fun Resources.mock(r: Int, v: String) { doReturn(v).`when`(this).getString(r) }
+private fun Resources.mock(r: Int, v: Array<String?>) { doReturn(v).`when`(this).getStringArray(r) }
+private fun Resources.mock(r: Int, v: IntArray) { doReturn(v).`when`(this).getIntArray(r) }
+
+internal fun initMockedResources(res: Resources) {
+    // Resources accessed through reflection need to return the id
+    doReturn(R.array.config_networkSupportedKeepaliveCount).`when`(res)
+            .getIdentifier(eq("config_networkSupportedKeepaliveCount"), eq("array"), any())
+    doReturn(R.array.network_switch_type_name).`when`(res)
+            .getIdentifier(eq("network_switch_type_name"), eq("array"), any())
+    // Mock the values themselves
+    res.mock(R.integer.config_networkTransitionTimeout, 60_000)
+    res.mock(R.string.config_networkCaptivePortalServerUrl, "")
+    res.mock(R.array.config_wakeonlan_supported_interfaces, arrayOf(WIFI_WOL_IFNAME))
+    res.mock(R.array.config_networkSupportedKeepaliveCount, arrayOf("0,1", "1,3"))
+    res.mock(R.array.config_networkNotifySwitches, arrayOfNulls<String>(size = 0))
+    res.mock(R.array.config_protectedNetworks, intArrayOf(10, 11, 12, 14, 15))
+    res.mock(R.array.network_switch_type_name, arrayOfNulls<String>(size = 0))
+    res.mock(R.integer.config_networkAvoidBadWifi, 1)
+    res.mock(R.integer.config_activelyPreferBadWifi, 0)
+    res.mock(R.bool.config_cellular_radio_timesharing_capable, true)
+}
+
+private val TEST_LINGER_DELAY_MS = 400
+private val TEST_NASCENT_DELAY_MS = 300
+internal fun makeConnectivityService(context: Context, deps: Dependencies) = ConnectivityService(
+        context,
+        mock<IDnsResolver>(),
+        mock<IpConnectivityLog>(),
+        mock<INetd>(),
+        deps).also {
+    it.mLingerDelayMs = TEST_LINGER_DELAY_MS
+    it.mNascentDelayMs = TEST_NASCENT_DELAY_MS
+}