Merge EthernetServiceTests into FrameworksNetTests

As per the TODO, merge EthernetServiceTests into the larger
FrameworksNetTests suite.

Similarly to NetworkStats, NSD or IpSec tests, the tests are also marked
as "non-connectivity-module-test", where "module" actually refers to
modules being built for release (from an S-based branch) today. This is
necessary as the tests and associated code cannot build without T APIs.

Also add FrameworksNetTests to presubmit as non-connectivity-module
tests are not run in presubmit without this.

Test: TH
Merged-In: Id533cdb4ac184b963f570af299dea04754ba88e9
Change-Id: I9950fcb49fdc2217134a59e993941cbe5da0b556
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
index 07dcae3..c27c973 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -76,6 +76,7 @@
         "java/com/android/server/IpSecServiceParameterizedTest.java",
         "java/com/android/server/IpSecServiceRefcountedResourceTest.java",
         "java/com/android/server/IpSecServiceTest.java",
+        "java/com/android/server/NativeDaemonConnectorTest.java",
         "java/com/android/server/NetworkManagementServiceTest.java",
         "java/com/android/server/NsdServiceTest.java",
         "java/com/android/server/connectivity/IpConnectivityEventBuilderTest.java",
@@ -83,8 +84,10 @@
         "java/com/android/server/connectivity/MultipathPolicyTrackerTest.java",
         "java/com/android/server/connectivity/NetdEventListenerServiceTest.java",
         "java/com/android/server/connectivity/VpnTest.java",
+        "java/com/android/server/ethernet/*.java",
         "java/com/android/server/net/ipmemorystore/*.java",
         "java/com/android/server/net/BpfInterfaceMapUpdaterTest.java",
+        "java/com/android/server/net/IpConfigStoreTest.java",
         "java/com/android/server/net/NetworkStats*.java",
         "java/com/android/server/net/TestableUsageCallback.kt",
     ]
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
new file mode 100644
index 0000000..4d3e4d3
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -0,0 +1,783 @@
+/*
+ * 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 com.android.server.ethernet;
+
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNotSame;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.same;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.annotation.NonNull;
+import android.app.test.MockAnswerUtil.AnswerWithArguments;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.ConnectivityManager;
+import android.net.EthernetNetworkSpecifier;
+import android.net.EthernetNetworkManagementException;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.IpConfiguration;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkRequest;
+import android.net.StaticIpConfiguration;
+import android.net.ip.IpClientCallbacks;
+import android.net.ip.IpClientManager;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.test.TestLooper;
+import android.util.Pair;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.connectivity.resources.R;
+import com.android.net.module.util.InterfaceParams;
+
+import com.android.testutils.DevSdkIgnoreRule;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class EthernetNetworkFactoryTest {
+    private static final int TIMEOUT_MS = 2_000;
+    private static final String TEST_IFACE = "test123";
+    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
+    private static final String IP_ADDR = "192.0.2.2/25";
+    private static final LinkAddress LINK_ADDR = new LinkAddress(IP_ADDR);
+    private static final String HW_ADDR = "01:02:03:04:05:06";
+    private TestLooper mLooper;
+    private Handler mHandler;
+    private EthernetNetworkFactory mNetFactory = null;
+    private IpClientCallbacks mIpClientCallbacks;
+    @Mock private Context mContext;
+    @Mock private Resources mResources;
+    @Mock private EthernetNetworkFactory.Dependencies mDeps;
+    @Mock private IpClientManager mIpClient;
+    @Mock private EthernetNetworkAgent mNetworkAgent;
+    @Mock private InterfaceParams mInterfaceParams;
+    @Mock private Network mMockNetwork;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        setupNetworkAgentMock();
+        setupIpClientMock();
+        setupContext();
+    }
+
+    //TODO: Move away from usage of TestLooper in order to move this logic back into @Before.
+    private void initEthernetNetworkFactory() {
+        mLooper = new TestLooper();
+        mHandler = new Handler(mLooper.getLooper());
+        mNetFactory = new EthernetNetworkFactory(mHandler, mContext, mDeps);
+    }
+
+    private void setupNetworkAgentMock() {
+        when(mDeps.makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any()))
+                .thenAnswer(new AnswerWithArguments() {
+                                       public EthernetNetworkAgent answer(
+                                               Context context,
+                                               Looper looper,
+                                               NetworkCapabilities nc,
+                                               LinkProperties lp,
+                                               NetworkAgentConfig config,
+                                               NetworkProvider provider,
+                                               EthernetNetworkAgent.Callbacks cb) {
+                                           when(mNetworkAgent.getCallbacks()).thenReturn(cb);
+                                           when(mNetworkAgent.getNetwork())
+                                                   .thenReturn(mMockNetwork);
+                                           return mNetworkAgent;
+                                       }
+                                   }
+        );
+    }
+
+    private void setupIpClientMock() throws Exception {
+        doAnswer(inv -> {
+            // these tests only support one concurrent IpClient, so make sure we do not accidentally
+            // create a mess.
+            assertNull("An IpClient has already been created.", mIpClientCallbacks);
+
+            mIpClientCallbacks = inv.getArgument(2);
+            mIpClientCallbacks.onIpClientCreated(null);
+            mLooper.dispatchAll();
+            return null;
+        }).when(mDeps).makeIpClient(any(Context.class), anyString(), any());
+
+        doAnswer(inv -> {
+            mIpClientCallbacks.onQuit();
+            mLooper.dispatchAll();
+            mIpClientCallbacks = null;
+            return null;
+        }).when(mIpClient).shutdown();
+
+        when(mDeps.makeIpClientManager(any())).thenReturn(mIpClient);
+    }
+
+    private void triggerOnProvisioningSuccess() {
+        mIpClientCallbacks.onProvisioningSuccess(new LinkProperties());
+        mLooper.dispatchAll();
+    }
+
+    private void triggerOnProvisioningFailure() {
+        mIpClientCallbacks.onProvisioningFailure(new LinkProperties());
+        mLooper.dispatchAll();
+    }
+
+    private void triggerOnReachabilityLost() {
+        mIpClientCallbacks.onReachabilityLost("ReachabilityLost");
+        mLooper.dispatchAll();
+    }
+
+    private void setupContext() {
+        when(mDeps.getTcpBufferSizesFromResource(eq(mContext))).thenReturn("");
+    }
+
+    @After
+    public void tearDown() {
+        // looper is shared with the network agents, so there may still be messages to dispatch on
+        // tear down.
+        mLooper.dispatchAll();
+    }
+
+    private NetworkCapabilities createDefaultFilterCaps() {
+        return NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .build();
+    }
+
+    private NetworkCapabilities.Builder createInterfaceCapsBuilder(final int transportType) {
+        return new NetworkCapabilities.Builder()
+                .addTransportType(transportType)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
+    }
+
+    private NetworkRequest.Builder createDefaultRequestBuilder() {
+        return new NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+    }
+
+    private NetworkRequest createDefaultRequest() {
+        return createDefaultRequestBuilder().build();
+    }
+
+    private IpConfiguration createDefaultIpConfig() {
+        IpConfiguration ipConfig = new IpConfiguration();
+        ipConfig.setIpAssignment(IpConfiguration.IpAssignment.DHCP);
+        ipConfig.setProxySettings(IpConfiguration.ProxySettings.NONE);
+        return ipConfig;
+    }
+
+    /**
+     * Create an {@link IpConfiguration} with an associated {@link StaticIpConfiguration}.
+     *
+     * @return {@link IpConfiguration} with its {@link StaticIpConfiguration} set.
+     */
+    private IpConfiguration createStaticIpConfig() {
+        final IpConfiguration ipConfig = new IpConfiguration();
+        ipConfig.setIpAssignment(IpConfiguration.IpAssignment.STATIC);
+        ipConfig.setStaticIpConfiguration(
+                new StaticIpConfiguration.Builder().setIpAddress(LINK_ADDR).build());
+        return ipConfig;
+    }
+
+    // creates an interface with provisioning in progress (since updating the interface link state
+    // automatically starts the provisioning process)
+    private void createInterfaceUndergoingProvisioning(String iface) {
+        // Default to the ethernet transport type.
+        createInterfaceUndergoingProvisioning(iface, NetworkCapabilities.TRANSPORT_ETHERNET);
+    }
+
+    private void createInterfaceUndergoingProvisioning(
+            @NonNull final String iface, final int transportType) {
+        final IpConfiguration ipConfig = createDefaultIpConfig();
+        mNetFactory.addInterface(iface, HW_ADDR, ipConfig,
+                createInterfaceCapsBuilder(transportType).build());
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true, NULL_LISTENER));
+        verifyStart(ipConfig);
+        clearInvocations(mDeps);
+        clearInvocations(mIpClient);
+    }
+
+    // creates a provisioned interface
+    private void createAndVerifyProvisionedInterface(String iface) throws Exception {
+        // Default to the ethernet transport type.
+        createAndVerifyProvisionedInterface(iface, NetworkCapabilities.TRANSPORT_ETHERNET,
+                ConnectivityManager.TYPE_ETHERNET);
+    }
+
+    private void createVerifyAndRemoveProvisionedInterface(final int transportType,
+            final int expectedLegacyType) throws Exception {
+        createAndVerifyProvisionedInterface(TEST_IFACE, transportType,
+                expectedLegacyType);
+        mNetFactory.removeInterface(TEST_IFACE);
+    }
+
+    private void createAndVerifyProvisionedInterface(
+            @NonNull final String iface, final int transportType, final int expectedLegacyType)
+            throws Exception {
+        createInterfaceUndergoingProvisioning(iface, transportType);
+        triggerOnProvisioningSuccess();
+        // provisioning succeeded, verify that the network agent is created, registered, marked
+        // as connected and legacy type are correctly set.
+        final ArgumentCaptor<NetworkCapabilities> ncCaptor = ArgumentCaptor.forClass(
+                NetworkCapabilities.class);
+        verify(mDeps).makeEthernetNetworkAgent(any(), any(), ncCaptor.capture(), any(),
+                argThat(x -> x.getLegacyType() == expectedLegacyType), any(), any());
+        assertEquals(
+                new EthernetNetworkSpecifier(iface), ncCaptor.getValue().getNetworkSpecifier());
+        verifyNetworkAgentRegistersAndConnects();
+        clearInvocations(mDeps);
+        clearInvocations(mNetworkAgent);
+    }
+
+    // creates an unprovisioned interface
+    private void createUnprovisionedInterface(String iface) throws Exception {
+        // To create an unprovisioned interface, provision and then "stop" it, i.e. stop its
+        // NetworkAgent and IpClient. One way this can be done is by provisioning an interface and
+        // then calling onNetworkUnwanted.
+        createAndVerifyProvisionedInterface(iface);
+
+        mNetworkAgent.getCallbacks().onNetworkUnwanted();
+        mLooper.dispatchAll();
+        verifyStop();
+
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+    }
+
+    @Test
+    public void testAcceptRequest() throws Exception {
+        initEthernetNetworkFactory();
+        createInterfaceUndergoingProvisioning(TEST_IFACE);
+        assertTrue(mNetFactory.acceptRequest(createDefaultRequest()));
+
+        NetworkRequest wifiRequest = createDefaultRequestBuilder()
+                .removeTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI).build();
+        assertFalse(mNetFactory.acceptRequest(wifiRequest));
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForActiveProvisioningInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createInterfaceUndergoingProvisioning(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        // verify that the IpClient gets shut down when interface state changes to down.
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+
+        assertTrue(ret);
+        verify(mIpClient).shutdown();
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForProvisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+
+        assertTrue(ret);
+        verifyStop();
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForUnprovisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createUnprovisionedInterface(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, false /* up */, listener);
+
+        assertTrue(ret);
+        // There should not be an active IPClient or NetworkAgent.
+        verify(mDeps, never()).makeIpClient(any(), any(), any());
+        verify(mDeps, never())
+                .makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any());
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForNonExistingInterface() throws Exception {
+        initEthernetNetworkFactory();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        // if interface was never added, link state cannot be updated.
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener);
+
+        assertFalse(ret);
+        verifyNoStopOrStart();
+        listener.expectOnErrorWithMessage("can't be updated as it is not available");
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateWithNoChanges() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        final boolean ret =
+                mNetFactory.updateInterfaceLinkState(TEST_IFACE, true /* up */, listener);
+
+        assertFalse(ret);
+        verifyNoStopOrStart();
+        listener.expectOnErrorWithMessage("No changes");
+    }
+
+    @Test
+    public void testNeedNetworkForOnProvisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient, never()).startProvisioning(any());
+    }
+
+    @Test
+    public void testNeedNetworkForOnUnprovisionedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        createUnprovisionedInterface(TEST_IFACE);
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient).startProvisioning(any());
+
+        triggerOnProvisioningSuccess();
+        verifyNetworkAgentRegistersAndConnects();
+    }
+
+    @Test
+    public void testNeedNetworkForOnInterfaceUndergoingProvisioning() throws Exception {
+        initEthernetNetworkFactory();
+        createInterfaceUndergoingProvisioning(TEST_IFACE);
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient, never()).startProvisioning(any());
+
+        triggerOnProvisioningSuccess();
+        verifyNetworkAgentRegistersAndConnects();
+    }
+
+    @Test
+    public void testProvisioningLoss() throws Exception {
+        initEthernetNetworkFactory();
+        when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        triggerOnProvisioningFailure();
+        verifyStop();
+        // provisioning loss should trigger a retry, since the interface is still there
+        verify(mIpClient).startProvisioning(any());
+    }
+
+    @Test
+    public void testProvisioningLossForDisappearedInterface() throws Exception {
+        initEthernetNetworkFactory();
+        // mocked method returns null by default, but just to be explicit in the test:
+        when(mDeps.getNetworkInterfaceByName(eq(TEST_IFACE))).thenReturn(null);
+
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        triggerOnProvisioningFailure();
+
+        // the interface disappeared and getNetworkInterfaceByName returns null, we should not retry
+        verify(mIpClient, never()).startProvisioning(any());
+        verifyNoStopOrStart();
+    }
+
+    private void verifyNoStopOrStart() {
+        verify(mNetworkAgent, never()).register();
+        verify(mIpClient, never()).shutdown();
+        verify(mNetworkAgent, never()).unregister();
+        verify(mIpClient, never()).startProvisioning(any());
+    }
+
+    @Test
+    public void testIpClientIsNotStartedWhenLinkIsDown() throws Exception {
+        initEthernetNetworkFactory();
+        createUnprovisionedInterface(TEST_IFACE);
+        mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER);
+
+        mNetFactory.needNetworkFor(createDefaultRequest());
+
+        verify(mDeps, never()).makeIpClient(any(), any(), any());
+
+        // BUG(b/191854824): requesting a network with a specifier (Android Auto use case) should
+        // not start an IpClient when the link is down, but fixing this may make matters worse by
+        // tiggering b/197548738.
+        NetworkRequest specificNetRequest = new NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .setNetworkSpecifier(new EthernetNetworkSpecifier(TEST_IFACE))
+                .build();
+        mNetFactory.needNetworkFor(specificNetRequest);
+        mNetFactory.releaseNetworkFor(specificNetRequest);
+
+        mNetFactory.updateInterfaceLinkState(TEST_IFACE, true, NULL_LISTENER);
+        // TODO: change to once when b/191854824 is fixed.
+        verify(mDeps, times(2)).makeIpClient(any(), eq(TEST_IFACE), any());
+    }
+
+    @Test
+    public void testLinkPropertiesChanged() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        LinkProperties lp = new LinkProperties();
+        mIpClientCallbacks.onLinkPropertiesChange(lp);
+        mLooper.dispatchAll();
+        verify(mNetworkAgent).sendLinkPropertiesImpl(same(lp));
+    }
+
+    @Test
+    public void testNetworkUnwanted() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        mNetworkAgent.getCallbacks().onNetworkUnwanted();
+        mLooper.dispatchAll();
+        verifyStop();
+    }
+
+    @Test
+    public void testNetworkUnwantedWithStaleNetworkAgent() throws Exception {
+        initEthernetNetworkFactory();
+        // ensures provisioning is restarted after provisioning loss
+        when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        EthernetNetworkAgent.Callbacks oldCbs = mNetworkAgent.getCallbacks();
+        // replace network agent in EthernetNetworkFactory
+        // Loss of provisioning will restart the ip client and network agent.
+        triggerOnProvisioningFailure();
+        verify(mDeps).makeIpClient(any(), any(), any());
+
+        triggerOnProvisioningSuccess();
+        verify(mDeps).makeEthernetNetworkAgent(any(), any(), any(), any(), any(), any(), any());
+
+        // verify that unwanted is ignored
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+        oldCbs.onNetworkUnwanted();
+        verify(mIpClient, never()).shutdown();
+        verify(mNetworkAgent, never()).unregister();
+    }
+
+    @Test
+    public void testTransportOverrideIsCorrectlySet() throws Exception {
+        initEthernetNetworkFactory();
+        // createProvisionedInterface() has verifications in place for transport override
+        // functionality which for EthernetNetworkFactory is network score and legacy type mappings.
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_ETHERNET,
+                ConnectivityManager.TYPE_ETHERNET);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_BLUETOOTH,
+                ConnectivityManager.TYPE_BLUETOOTH);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_WIFI,
+                ConnectivityManager.TYPE_WIFI);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_CELLULAR,
+                ConnectivityManager.TYPE_MOBILE);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_LOWPAN,
+                ConnectivityManager.TYPE_NONE);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_WIFI_AWARE,
+                ConnectivityManager.TYPE_NONE);
+        createVerifyAndRemoveProvisionedInterface(NetworkCapabilities.TRANSPORT_TEST,
+                ConnectivityManager.TYPE_NONE);
+    }
+
+    @Test
+    public void testReachabilityLoss() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        triggerOnReachabilityLost();
+
+        // Reachability loss should trigger a stop and start, since the interface is still there
+        verifyRestart(createDefaultIpConfig());
+    }
+
+    private IpClientCallbacks getStaleIpClientCallbacks() throws Exception {
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final IpClientCallbacks staleIpClientCallbacks = mIpClientCallbacks;
+        mNetFactory.removeInterface(TEST_IFACE);
+        verifyStop();
+        assertNotSame(mIpClientCallbacks, staleIpClientCallbacks);
+        return staleIpClientCallbacks;
+    }
+
+    @Test
+    public void testIgnoreOnIpLayerStartedCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+
+        staleIpClientCallbacks.onProvisioningSuccess(new LinkProperties());
+        mLooper.dispatchAll();
+
+        verify(mIpClient, never()).startProvisioning(any());
+        verify(mNetworkAgent, never()).register();
+    }
+
+    @Test
+    public void testIgnoreOnIpLayerStoppedCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        when(mDeps.getNetworkInterfaceByName(TEST_IFACE)).thenReturn(mInterfaceParams);
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+
+        staleIpClientCallbacks.onProvisioningFailure(new LinkProperties());
+        mLooper.dispatchAll();
+
+        verify(mIpClient, never()).startProvisioning(any());
+    }
+
+    @Test
+    public void testIgnoreLinkPropertiesCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+        final LinkProperties lp = new LinkProperties();
+
+        staleIpClientCallbacks.onLinkPropertiesChange(lp);
+        mLooper.dispatchAll();
+
+        verify(mNetworkAgent, never()).sendLinkPropertiesImpl(eq(lp));
+    }
+
+    @Test
+    public void testIgnoreNeighborLossCallbackForStaleCallback() throws Exception {
+        initEthernetNetworkFactory();
+        final IpClientCallbacks staleIpClientCallbacks = getStaleIpClientCallbacks();
+
+        staleIpClientCallbacks.onReachabilityLost("Neighbor Lost");
+        mLooper.dispatchAll();
+
+        verify(mIpClient, never()).startProvisioning(any());
+        verify(mNetworkAgent, never()).register();
+    }
+
+    private void verifyRestart(@NonNull final IpConfiguration ipConfig) {
+        verifyStop();
+        verifyStart(ipConfig);
+    }
+
+    private void verifyStart(@NonNull final IpConfiguration ipConfig) {
+        verify(mDeps).makeIpClient(any(Context.class), anyString(), any());
+        verify(mIpClient).startProvisioning(
+                argThat(x -> Objects.equals(x.mStaticIpConfig, ipConfig.getStaticIpConfiguration()))
+        );
+    }
+
+    private void verifyStop() {
+        verify(mIpClient).shutdown();
+        verify(mNetworkAgent).unregister();
+    }
+
+    private void verifyNetworkAgentRegistersAndConnects() {
+        verify(mNetworkAgent).register();
+        verify(mNetworkAgent).markConnected();
+    }
+
+    private static final class TestNetworkManagementListener
+            implements INetworkInterfaceOutcomeReceiver {
+        private final CompletableFuture<String> mResult = new CompletableFuture<>();
+        private final CompletableFuture<EthernetNetworkManagementException> mError =
+                new CompletableFuture<>();
+
+        @Override
+        public void onResult(@NonNull String iface) {
+            mResult.complete(iface);
+        }
+
+        @Override
+        public void onError(@NonNull EthernetNetworkManagementException exception) {
+            mError.complete(exception);
+        }
+
+        String expectOnResult() throws Exception {
+            return mResult.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+
+        EthernetNetworkManagementException expectOnError() throws Exception {
+            return mError.get(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+        }
+
+        void expectOnErrorWithMessage(String msg) throws Exception {
+            assertTrue(expectOnError().getMessage().contains(msg));
+        }
+
+        @Override
+        public IBinder asBinder() {
+            return null;
+        }
+    }
+
+    @Test
+    public void testUpdateInterfaceCallsListenerCorrectlyOnSuccess() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+        triggerOnProvisioningSuccess();
+
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceAbortsOnConcurrentRemoveInterface() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetFactory.removeInterface(TEST_IFACE));
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceAbortsOnConcurrentUpdateInterfaceLinkState() throws Exception {
+        initEthernetNetworkFactory();
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> mNetFactory.updateInterfaceLinkState(TEST_IFACE, false, NULL_LISTENER));
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2) // TODO: Use to Build.VERSION_CODES.SC_V2 when available
+    @Test
+    public void testUpdateInterfaceCallsListenerCorrectlyOnConcurrentRequests() throws Exception {
+        initEthernetNetworkFactory();
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener successfulListener =
+                new TestNetworkManagementListener();
+
+        // If two calls come in before the first one completes, the first listener will be aborted
+        // and the second one will be successful.
+        verifyNetworkManagementCallIsAbortedWhenInterrupted(
+                TEST_IFACE,
+                () -> {
+                    mNetFactory.updateInterface(
+                            TEST_IFACE, ipConfiguration, capabilities, successfulListener);
+                    triggerOnProvisioningSuccess();
+                });
+
+        assertEquals(successfulListener.expectOnResult(), TEST_IFACE);
+    }
+
+    private void verifyNetworkManagementCallIsAbortedWhenInterrupted(
+            @NonNull final String iface,
+            @NonNull final Runnable interruptingRunnable) throws Exception {
+        createAndVerifyProvisionedInterface(iface);
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener failedListener = new TestNetworkManagementListener();
+
+        // An active update request will be aborted on interrupt prior to provisioning completion.
+        mNetFactory.updateInterface(iface, ipConfiguration, capabilities, failedListener);
+        interruptingRunnable.run();
+
+        failedListener.expectOnErrorWithMessage("aborted");
+    }
+
+    @Test
+    public void testUpdateInterfaceRestartsAgentCorrectly() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+        triggerOnProvisioningSuccess();
+
+        assertEquals(listener.expectOnResult(), TEST_IFACE);
+        verify(mDeps).makeEthernetNetworkAgent(any(), any(),
+                eq(capabilities), any(), any(), any(), any());
+        verifyRestart(ipConfiguration);
+    }
+
+    @Test
+    public void testUpdateInterfaceForNonExistingInterface() throws Exception {
+        initEthernetNetworkFactory();
+        // No interface exists due to not calling createAndVerifyProvisionedInterface(...).
+        final NetworkCapabilities capabilities = createDefaultFilterCaps();
+        final IpConfiguration ipConfiguration = createStaticIpConfig();
+        final TestNetworkManagementListener listener = new TestNetworkManagementListener();
+
+        mNetFactory.updateInterface(TEST_IFACE, ipConfiguration, capabilities, listener);
+
+        verifyNoStopOrStart();
+        listener.expectOnErrorWithMessage("can't be updated as it is not available");
+    }
+
+    @Test
+    public void testUpdateInterfaceWithNullIpConfiguration() throws Exception {
+        initEthernetNetworkFactory();
+        createAndVerifyProvisionedInterface(TEST_IFACE);
+
+        final IpConfiguration initialIpConfig = createStaticIpConfig();
+        mNetFactory.updateInterface(TEST_IFACE, initialIpConfig, null /*capabilities*/,
+                null /*listener*/);
+        triggerOnProvisioningSuccess();
+        verifyRestart(initialIpConfig);
+
+        // TODO: have verifyXyz functions clear invocations.
+        clearInvocations(mDeps);
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+
+
+        // verify that sending a null ipConfig does not update the current ipConfig.
+        mNetFactory.updateInterface(TEST_IFACE, null /*ipConfig*/, null /*capabilities*/,
+                null /*listener*/);
+        triggerOnProvisioningSuccess();
+        verifyRestart(initialIpConfig);
+    }
+}
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
new file mode 100644
index 0000000..dd1f1ed
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetServiceImplTest.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2021 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.server.ethernet;
+
+import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.fail;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.EthernetNetworkUpdateRequest;
+import android.net.IpConfiguration;
+import android.net.NetworkCapabilities;
+import android.os.Handler;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class EthernetServiceImplTest {
+    private static final String TEST_IFACE = "test123";
+    private static final EthernetNetworkUpdateRequest UPDATE_REQUEST =
+            new EthernetNetworkUpdateRequest.Builder()
+                    .setIpConfiguration(new IpConfiguration())
+                    .setNetworkCapabilities(new NetworkCapabilities.Builder().build())
+                    .build();
+    private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_CAPABILITIES =
+            new EthernetNetworkUpdateRequest.Builder()
+                    .setIpConfiguration(new IpConfiguration())
+                    .build();
+    private static final EthernetNetworkUpdateRequest UPDATE_REQUEST_WITHOUT_IP_CONFIG =
+            new EthernetNetworkUpdateRequest.Builder()
+                    .setNetworkCapabilities(new NetworkCapabilities.Builder().build())
+                    .build();
+    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
+    private EthernetServiceImpl mEthernetServiceImpl;
+    @Mock private Context mContext;
+    @Mock private Handler mHandler;
+    @Mock private EthernetTracker mEthernetTracker;
+    @Mock private PackageManager mPackageManager;
+
+    @Before
+    public void setup() {
+        MockitoAnnotations.initMocks(this);
+        doReturn(mPackageManager).when(mContext).getPackageManager();
+        mEthernetServiceImpl = new EthernetServiceImpl(mContext, mHandler, mEthernetTracker);
+        mEthernetServiceImpl.mStarted.set(true);
+        toggleAutomotiveFeature(true);
+        shouldTrackIface(TEST_IFACE, true);
+    }
+
+    private void toggleAutomotiveFeature(final boolean isEnabled) {
+        doReturn(isEnabled)
+                .when(mPackageManager).hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE);
+    }
+
+    private void shouldTrackIface(@NonNull final String iface, final boolean shouldTrack) {
+        doReturn(shouldTrack).when(mEthernetTracker).isTrackingInterface(iface);
+    }
+
+    @Test
+    public void testSetConfigurationRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.setConfiguration("" /* iface */, new IpConfiguration());
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(
+                    "" /* iface */, UPDATE_REQUEST, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.connectNetwork("" /* iface */, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsWhenEthNotStarted() {
+        mEthernetServiceImpl.mStarted.set(false);
+        assertThrows(IllegalStateException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork("" /* iface */, null /* listener */);
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(null, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.connectNetwork(null /* iface */, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsNullIface() {
+        assertThrows(NullPointerException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork(null /* iface */, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationWithCapabilitiesRejectsWithoutAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        assertThrows(UnsupportedOperationException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testUpdateConfigurationWithCapabilitiesWithAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_CAPABILITIES,
+                NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
+                eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getIpConfiguration()),
+                eq(UPDATE_REQUEST_WITHOUT_CAPABILITIES.getNetworkCapabilities()), isNull());
+    }
+
+    @Test
+    public void testConnectNetworkRejectsWithoutAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        assertThrows(UnsupportedOperationException.class, () -> {
+            mEthernetServiceImpl.connectNetwork("" /* iface */, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsWithoutAutomotiveFeature() {
+        toggleAutomotiveFeature(false);
+        assertThrows(UnsupportedOperationException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork("" /* iface */, NULL_LISTENER);
+        });
+    }
+
+    private void denyManageEthPermission() {
+        doThrow(new SecurityException("")).when(mContext)
+                .enforceCallingOrSelfPermission(
+                        eq(Manifest.permission.MANAGE_ETHERNET_NETWORKS), anyString());
+    }
+
+    private void denyManageTestNetworksPermission() {
+        doThrow(new SecurityException("")).when(mContext)
+                .enforceCallingOrSelfPermission(
+                        eq(Manifest.permission.MANAGE_TEST_NETWORKS), anyString());
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsWithoutManageEthPermission() {
+        denyManageEthPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsWithoutManageEthPermission() {
+        denyManageEthPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsWithoutManageEthPermission() {
+        denyManageEthPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    private void enableTestInterface() {
+        when(mEthernetTracker.isValidTestInterface(eq(TEST_IFACE))).thenReturn(true);
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsTestRequestWithoutTestPermission() {
+        enableTestInterface();
+        denyManageTestNetworksPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testConnectNetworkRejectsTestRequestWithoutTestPermission() {
+        enableTestInterface();
+        denyManageTestNetworksPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testDisconnectNetworkRejectsTestRequestWithoutTestPermission() {
+        enableTestInterface();
+        denyManageTestNetworksPermission();
+        assertThrows(SecurityException.class, () -> {
+            mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        });
+    }
+
+    @Test
+    public void testUpdateConfiguration() {
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(
+                eq(TEST_IFACE),
+                eq(UPDATE_REQUEST.getIpConfiguration()),
+                eq(UPDATE_REQUEST.getNetworkCapabilities()), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testConnectNetwork() {
+        mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisconnectNetwork() {
+        mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testUpdateConfigurationAcceptsTestRequestWithNullCapabilities() {
+        enableTestInterface();
+        final EthernetNetworkUpdateRequest request =
+                new EthernetNetworkUpdateRequest
+                        .Builder()
+                        .setIpConfiguration(new IpConfiguration()).build();
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, request, NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
+                eq(request.getIpConfiguration()),
+                eq(request.getNetworkCapabilities()), isNull());
+    }
+
+    @Test
+    public void testUpdateConfigurationAcceptsRequestWithNullIpConfiguration() {
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST_WITHOUT_IP_CONFIG,
+                NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(eq(TEST_IFACE),
+                eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getIpConfiguration()),
+                eq(UPDATE_REQUEST_WITHOUT_IP_CONFIG.getNetworkCapabilities()), isNull());
+    }
+
+    @Test
+    public void testUpdateConfigurationRejectsInvalidTestRequest() {
+        enableTestInterface();
+        assertThrows(IllegalArgumentException.class, () -> {
+            mEthernetServiceImpl.updateConfiguration(TEST_IFACE, UPDATE_REQUEST, NULL_LISTENER);
+        });
+    }
+
+    private EthernetNetworkUpdateRequest createTestNetworkUpdateRequest() {
+        final NetworkCapabilities nc =  new NetworkCapabilities
+                .Builder(UPDATE_REQUEST.getNetworkCapabilities())
+                .addTransportType(TRANSPORT_TEST).build();
+
+        return new EthernetNetworkUpdateRequest
+                .Builder(UPDATE_REQUEST)
+                .setNetworkCapabilities(nc).build();
+    }
+
+    @Test
+    public void testUpdateConfigurationForTestRequestDoesNotRequireAutoOrEthernetPermission() {
+        enableTestInterface();
+        toggleAutomotiveFeature(false);
+        denyManageEthPermission();
+        final EthernetNetworkUpdateRequest request = createTestNetworkUpdateRequest();
+
+        mEthernetServiceImpl.updateConfiguration(TEST_IFACE, request, NULL_LISTENER);
+        verify(mEthernetTracker).updateConfiguration(
+                eq(TEST_IFACE),
+                eq(request.getIpConfiguration()),
+                eq(request.getNetworkCapabilities()), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testConnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() {
+        enableTestInterface();
+        toggleAutomotiveFeature(false);
+        denyManageEthPermission();
+
+        mEthernetServiceImpl.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).connectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisconnectNetworkForTestRequestDoesNotRequireAutoOrNetPermission() {
+        enableTestInterface();
+        toggleAutomotiveFeature(false);
+        denyManageEthPermission();
+
+        mEthernetServiceImpl.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        verify(mEthernetTracker).disconnectNetwork(eq(TEST_IFACE), eq(NULL_LISTENER));
+    }
+
+    private void denyPermissions(String... permissions) {
+        for (String permission: permissions) {
+            doReturn(PackageManager.PERMISSION_DENIED).when(mContext)
+                    .checkCallingOrSelfPermission(eq(permission));
+        }
+    }
+
+    @Test
+    public void testSetEthernetEnabled() {
+        denyPermissions(android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK);
+        mEthernetServiceImpl.setEthernetEnabled(true);
+        verify(mEthernetTracker).setEthernetEnabled(true);
+        reset(mEthernetTracker);
+
+        denyPermissions(Manifest.permission.NETWORK_STACK);
+        mEthernetServiceImpl.setEthernetEnabled(false);
+        verify(mEthernetTracker).setEthernetEnabled(false);
+        reset(mEthernetTracker);
+
+        denyPermissions(Manifest.permission.NETWORK_SETTINGS);
+        try {
+            mEthernetServiceImpl.setEthernetEnabled(true);
+            fail("Should get SecurityException");
+        } catch (SecurityException e) { }
+        verify(mEthernetTracker, never()).setEthernetEnabled(false);
+    }
+}
diff --git a/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
new file mode 100644
index 0000000..b1831c4
--- /dev/null
+++ b/tests/unit/java/com/android/server/ethernet/EthernetTrackerTest.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) 2018 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.server.ethernet;
+
+import static android.net.TestNetworkManager.TEST_TAP_PREFIX;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.EthernetManager;
+import android.net.InetAddresses;
+import android.net.INetworkInterfaceOutcomeReceiver;
+import android.net.IEthernetServiceListener;
+import android.net.INetd;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.InterfaceConfigurationParcel;
+import android.net.LinkAddress;
+import android.net.NetworkCapabilities;
+import android.net.StaticIpConfiguration;
+import android.os.HandlerThread;
+import android.os.RemoteException;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.connectivity.resources.R;
+import com.android.testutils.HandlerUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class EthernetTrackerTest {
+    private static final String TEST_IFACE = "test123";
+    private static final int TIMEOUT_MS = 1_000;
+    private static final String THREAD_NAME = "EthernetServiceThread";
+    private static final INetworkInterfaceOutcomeReceiver NULL_LISTENER = null;
+    private EthernetTracker tracker;
+    private HandlerThread mHandlerThread;
+    @Mock private Context mContext;
+    @Mock private EthernetNetworkFactory mFactory;
+    @Mock private INetd mNetd;
+    @Mock private EthernetTracker.Dependencies mDeps;
+
+    @Before
+    public void setUp() throws RemoteException {
+        MockitoAnnotations.initMocks(this);
+        initMockResources();
+        when(mFactory.updateInterfaceLinkState(anyString(), anyBoolean(), any())).thenReturn(false);
+        when(mNetd.interfaceGetList()).thenReturn(new String[0]);
+        mHandlerThread = new HandlerThread(THREAD_NAME);
+        mHandlerThread.start();
+        tracker = new EthernetTracker(mContext, mHandlerThread.getThreadHandler(), mFactory, mNetd,
+                mDeps);
+    }
+
+    @After
+    public void cleanUp() {
+        mHandlerThread.quitSafely();
+    }
+
+    private void initMockResources() {
+        when(mDeps.getInterfaceRegexFromResource(eq(mContext))).thenReturn("");
+        when(mDeps.getInterfaceConfigFromResource(eq(mContext))).thenReturn(new String[0]);
+    }
+
+    private void waitForIdle() {
+        HandlerUtils.waitForIdle(mHandlerThread, TIMEOUT_MS);
+    }
+
+    /**
+     * Test: Creation of various valid static IP configurations
+     */
+    @Test
+    public void createStaticIpConfiguration() {
+        // Empty gives default StaticIPConfiguration object
+        assertStaticConfiguration(new StaticIpConfiguration(), "");
+
+        // Setting only the IP address properly cascades and assumes defaults
+        assertStaticConfiguration(new StaticIpConfiguration.Builder()
+                .setIpAddress(new LinkAddress("192.0.2.10/24")).build(), "ip=192.0.2.10/24");
+
+        final ArrayList<InetAddress> dnsAddresses = new ArrayList<>();
+        dnsAddresses.add(InetAddresses.parseNumericAddress("4.4.4.4"));
+        dnsAddresses.add(InetAddresses.parseNumericAddress("8.8.8.8"));
+        // Setting other fields properly cascades them
+        assertStaticConfiguration(new StaticIpConfiguration.Builder()
+                .setIpAddress(new LinkAddress("192.0.2.10/24"))
+                .setDnsServers(dnsAddresses)
+                .setGateway(InetAddresses.parseNumericAddress("192.0.2.1"))
+                .setDomains("android").build(),
+                "ip=192.0.2.10/24 dns=4.4.4.4,8.8.8.8 gateway=192.0.2.1 domains=android");
+
+        // Verify order doesn't matter
+        assertStaticConfiguration(new StaticIpConfiguration.Builder()
+                .setIpAddress(new LinkAddress("192.0.2.10/24"))
+                .setDnsServers(dnsAddresses)
+                .setGateway(InetAddresses.parseNumericAddress("192.0.2.1"))
+                .setDomains("android").build(),
+                "domains=android ip=192.0.2.10/24 gateway=192.0.2.1 dns=4.4.4.4,8.8.8.8 ");
+    }
+
+    /**
+     * Test: Attempt creation of various bad static IP configurations
+     */
+    @Test
+    public void createStaticIpConfiguration_Bad() {
+        assertStaticConfigurationFails("ip=192.0.2.1/24 gateway= blah=20.20.20.20");  // Unknown key
+        assertStaticConfigurationFails("ip=192.0.2.1");  // mask is missing
+        assertStaticConfigurationFails("ip=a.b.c");  // not a valid ip address
+        assertStaticConfigurationFails("dns=4.4.4.4,1.2.3.A");  // not valid ip address in dns
+        assertStaticConfigurationFails("=");  // Key and value is empty
+        assertStaticConfigurationFails("ip=");  // Value is empty
+        assertStaticConfigurationFails("ip=192.0.2.1/24 gateway=");  // Gateway is empty
+    }
+
+    private void assertStaticConfigurationFails(String config) {
+        try {
+            EthernetTracker.parseStaticIpConfiguration(config);
+            fail("Expected to fail: " + config);
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+
+    private void assertStaticConfiguration(StaticIpConfiguration expectedStaticIpConfig,
+                String configAsString) {
+        final IpConfiguration expectedIpConfiguration = new IpConfiguration();
+        expectedIpConfiguration.setIpAssignment(IpAssignment.STATIC);
+        expectedIpConfiguration.setProxySettings(ProxySettings.NONE);
+        expectedIpConfiguration.setStaticIpConfiguration(expectedStaticIpConfig);
+
+        assertEquals(expectedIpConfiguration,
+                EthernetTracker.parseStaticIpConfiguration(configAsString));
+    }
+
+    private NetworkCapabilities.Builder makeEthernetCapabilitiesBuilder(boolean clearAll) {
+        final NetworkCapabilities.Builder builder =
+                clearAll ? NetworkCapabilities.Builder.withoutDefaultCapabilities()
+                        : new NetworkCapabilities.Builder();
+        return builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
+                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED);
+    }
+
+    /**
+     * Test: Attempt to create a capabilties with various valid sets of capabilities/transports
+     */
+    @Test
+    public void createNetworkCapabilities() {
+
+        // Particularly common expected results
+        NetworkCapabilities defaultEthernetCleared =
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .build();
+
+        NetworkCapabilities ethernetClearedWithCommonCaps =
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .addCapability(12)
+                        .addCapability(13)
+                        .addCapability(14)
+                        .addCapability(15)
+                        .build();
+
+        // Empty capabilities and transports lists with a "please clear defaults" should
+        // yield an empty capabilities set with TRANPORT_ETHERNET
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "");
+
+        // Empty capabilities and transports without the clear defaults flag should return the
+        // default capabilities set with TRANSPORT_ETHERNET
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(false /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .build(),
+                false, "", "");
+
+        // A list of capabilities without the clear defaults flag should return the default
+        // capabilities, mixed with the desired capabilities, and TRANSPORT_ETHERNET
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(false /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                        .addCapability(11)
+                        .addCapability(12)
+                        .build(),
+                false, "11,12", "");
+
+        // Adding a list of capabilities with a clear defaults will leave exactly those capabilities
+        // with a default TRANSPORT_ETHERNET since no overrides are specified
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15", "");
+
+        // Adding any invalid capabilities to the list will cause them to be ignored
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,65,73", "");
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "12,13,14,15,abcdefg", "");
+
+        // Adding a valid override transport will remove the default TRANSPORT_ETHERNET transport
+        // and apply only the override to the capabiltities object
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(0)
+                        .build(),
+                true, "", "0");
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(1)
+                        .build(),
+                true, "", "1");
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(2)
+                        .build(),
+                true, "", "2");
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addTransportType(3)
+                        .build(),
+                true, "", "3");
+
+        // "4" is TRANSPORT_VPN, which is unsupported. Should default back to TRANPORT_ETHERNET
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "4");
+
+        // "5" is TRANSPORT_WIFI_AWARE, which is currently supported due to no legacy TYPE_NONE
+        // conversion. When that becomes available, this test must be updated
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "5");
+
+        // "6" is TRANSPORT_LOWPAN, which is currently supported due to no legacy TYPE_NONE
+        // conversion. When that becomes available, this test must be updated
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "6");
+
+        // Adding an invalid override transport will leave the transport as TRANSPORT_ETHERNET
+        assertParsedNetworkCapabilities(defaultEthernetCleared,true, "", "100");
+        assertParsedNetworkCapabilities(defaultEthernetCleared, true, "", "abcdefg");
+
+        // Ensure the adding of both capabilities and transports work
+        assertParsedNetworkCapabilities(
+                makeEthernetCapabilitiesBuilder(true /* clearAll */)
+                        .setLinkUpstreamBandwidthKbps(100000)
+                        .setLinkDownstreamBandwidthKbps(100000)
+                        .addCapability(12)
+                        .addCapability(13)
+                        .addCapability(14)
+                        .addCapability(15)
+                        .addTransportType(3)
+                        .build(),
+                true, "12,13,14,15", "3");
+
+        // Ensure order does not matter for capability list
+        assertParsedNetworkCapabilities(ethernetClearedWithCommonCaps, true, "13,12,15,14", "");
+    }
+
+    private void assertParsedNetworkCapabilities(NetworkCapabilities expectedNetworkCapabilities,
+            boolean clearCapabilties, String configCapabiltiies,String configTransports) {
+        assertEquals(expectedNetworkCapabilities,
+                EthernetTracker.createNetworkCapabilities(clearCapabilties, configCapabiltiies,
+                        configTransports).build());
+    }
+
+    @Test
+    public void testCreateEthernetTrackerConfigReturnsCorrectValue() {
+        final String capabilities = "2";
+        final String ipConfig = "3";
+        final String transport = "4";
+        final String configString = String.join(";", TEST_IFACE, capabilities, ipConfig, transport);
+
+        final EthernetTracker.EthernetTrackerConfig config =
+                EthernetTracker.createEthernetTrackerConfig(configString);
+
+        assertEquals(TEST_IFACE, config.mIface);
+        assertEquals(capabilities, config.mCapabilities);
+        assertEquals(ipConfig, config.mIpConfig);
+        assertEquals(transport, config.mTransport);
+    }
+
+    @Test
+    public void testCreateEthernetTrackerConfigThrowsNpeWithNullInput() {
+        assertThrows(NullPointerException.class,
+                () -> EthernetTracker.createEthernetTrackerConfig(null));
+    }
+
+    @Test
+    public void testUpdateConfiguration() {
+        final NetworkCapabilities capabilities = new NetworkCapabilities.Builder().build();
+        final LinkAddress linkAddr = new LinkAddress("192.0.2.2/25");
+        final StaticIpConfiguration staticIpConfig =
+                new StaticIpConfiguration.Builder().setIpAddress(linkAddr).build();
+        final IpConfiguration ipConfig =
+                new IpConfiguration.Builder().setStaticIpConfiguration(staticIpConfig).build();
+        final INetworkInterfaceOutcomeReceiver listener = null;
+
+        tracker.updateConfiguration(TEST_IFACE, ipConfig, capabilities, listener);
+        waitForIdle();
+
+        verify(mFactory).updateInterface(
+                eq(TEST_IFACE), eq(ipConfig), eq(capabilities), eq(listener));
+    }
+
+    @Test
+    public void testConnectNetworkCorrectlyCallsFactory() {
+        tracker.connectNetwork(TEST_IFACE, NULL_LISTENER);
+        waitForIdle();
+
+        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(true /* up */),
+                eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testDisconnectNetworkCorrectlyCallsFactory() {
+        tracker.disconnectNetwork(TEST_IFACE, NULL_LISTENER);
+        waitForIdle();
+
+        verify(mFactory).updateInterfaceLinkState(eq(TEST_IFACE), eq(false /* up */),
+                eq(NULL_LISTENER));
+    }
+
+    @Test
+    public void testIsValidTestInterfaceIsFalseWhenTestInterfacesAreNotIncluded() {
+        final String validIfaceName = TEST_TAP_PREFIX + "123";
+        tracker.setIncludeTestInterfaces(false);
+        waitForIdle();
+
+        final boolean isValidTestInterface = tracker.isValidTestInterface(validIfaceName);
+
+        assertFalse(isValidTestInterface);
+    }
+
+    @Test
+    public void testIsValidTestInterfaceIsFalseWhenTestInterfaceNameIsInvalid() {
+        final String invalidIfaceName = "123" + TEST_TAP_PREFIX;
+        tracker.setIncludeTestInterfaces(true);
+        waitForIdle();
+
+        final boolean isValidTestInterface = tracker.isValidTestInterface(invalidIfaceName);
+
+        assertFalse(isValidTestInterface);
+    }
+
+    @Test
+    public void testIsValidTestInterfaceIsTrueWhenTestInterfacesIncludedAndValidName() {
+        final String validIfaceName = TEST_TAP_PREFIX + "123";
+        tracker.setIncludeTestInterfaces(true);
+        waitForIdle();
+
+        final boolean isValidTestInterface = tracker.isValidTestInterface(validIfaceName);
+
+        assertTrue(isValidTestInterface);
+    }
+
+    public static class EthernetStateListener extends IEthernetServiceListener.Stub {
+        @Override
+        public void onEthernetStateChanged(int state) { }
+
+        @Override
+        public void onInterfaceStateChanged(String iface, int state, int role,
+                IpConfiguration configuration) { }
+    }
+
+    @Test
+    public void testListenEthernetStateChange() throws Exception {
+        final String testIface = "testtap123";
+        final String testHwAddr = "11:22:33:44:55:66";
+        final InterfaceConfigurationParcel ifaceParcel = new InterfaceConfigurationParcel();
+        ifaceParcel.ifName = testIface;
+        ifaceParcel.hwAddr = testHwAddr;
+        ifaceParcel.flags = new String[] {INetd.IF_STATE_UP};
+
+        tracker.setIncludeTestInterfaces(true);
+        waitForIdle();
+
+        when(mNetd.interfaceGetList()).thenReturn(new String[] {testIface});
+        when(mNetd.interfaceGetCfg(eq(testIface))).thenReturn(ifaceParcel);
+        doReturn(new String[] {testIface}).when(mFactory).getAvailableInterfaces(anyBoolean());
+        doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface));
+
+        final EthernetStateListener listener = spy(new EthernetStateListener());
+        tracker.addListener(listener, true /* canUseRestrictedNetworks */);
+        // Check default state.
+        waitForIdle();
+        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP),
+                anyInt(), any());
+        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED));
+        reset(listener);
+
+        doReturn(EthernetManager.STATE_ABSENT).when(mFactory).getInterfaceState(eq(testIface));
+        tracker.setEthernetEnabled(false);
+        waitForIdle();
+        verify(mFactory).removeInterface(eq(testIface));
+        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_DISABLED));
+        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_ABSENT),
+                anyInt(), any());
+        reset(listener);
+
+        doReturn(EthernetManager.STATE_LINK_UP).when(mFactory).getInterfaceState(eq(testIface));
+        tracker.setEthernetEnabled(true);
+        waitForIdle();
+        verify(mFactory).addInterface(eq(testIface), eq(testHwAddr), any(), any());
+        verify(listener).onEthernetStateChanged(eq(EthernetManager.ETHERNET_STATE_ENABLED));
+        verify(listener).onInterfaceStateChanged(eq(testIface), eq(EthernetManager.STATE_LINK_UP),
+                anyInt(), any());
+    }
+}