Add Tests for EthernetNetworkFactory

Test: atest EthernetNetworkFactoryTest
Bug: 191635995
Change-Id: I1c07bb6d30706c4e13002eb402fadfecb97b36d1
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java b/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java
new file mode 100644
index 0000000..5598fc6
--- /dev/null
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkAgent.java
@@ -0,0 +1,65 @@
+/*
+ * 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 android.content.Context;
+import android.net.LinkProperties;
+import android.net.NetworkAgent;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.os.Looper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+public class EthernetNetworkAgent extends NetworkAgent {
+
+    private static final String TAG = "EthernetNetworkAgent";
+
+    public interface Callbacks {
+        void onNetworkUnwanted();
+    }
+
+    private final Callbacks mCallbacks;
+
+    EthernetNetworkAgent(
+            @NonNull Context context,
+            @NonNull Looper looper,
+            @NonNull NetworkCapabilities nc,
+            @NonNull LinkProperties lp,
+            int networkScore,
+            @NonNull NetworkAgentConfig config,
+            @Nullable NetworkProvider provider,
+            @NonNull Callbacks cb) {
+        super(context, looper, TAG, nc, lp, networkScore, config, provider);
+        mCallbacks = cb;
+    }
+
+    @Override
+    public void onNetworkUnwanted() {
+        mCallbacks.onNetworkUnwanted();
+    }
+
+    // sendLinkProperties is final in NetworkAgent, so it cannot be mocked.
+    public void sendLinkPropertiesImpl(LinkProperties lp) {
+        sendLinkProperties(lp);
+    }
+
+    public Callbacks getCallbacks() {
+        return mCallbacks;
+    }
+}
diff --git a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
index 28b24f1..f4de23d 100644
--- a/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/service-t/src/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -27,10 +27,10 @@
 import android.net.IpConfiguration.IpAssignment;
 import android.net.IpConfiguration.ProxySettings;
 import android.net.LinkProperties;
-import android.net.NetworkAgent;
 import android.net.NetworkAgentConfig;
 import android.net.NetworkCapabilities;
 import android.net.NetworkFactory;
+import android.net.NetworkProvider;
 import android.net.NetworkRequest;
 import android.net.NetworkSpecifier;
 import android.net.ip.IIpClient;
@@ -40,12 +40,14 @@
 import android.net.util.InterfaceParams;
 import android.os.ConditionVariable;
 import android.os.Handler;
+import android.os.Looper;
 import android.os.RemoteException;
 import android.text.TextUtils;
 import android.util.AndroidRuntimeException;
 import android.util.Log;
 import android.util.SparseArray;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.IndentingPrintWriter;
 
 import java.io.FileDescriptor;
@@ -69,6 +71,25 @@
             new ConcurrentHashMap<>();
     private final Handler mHandler;
     private final Context mContext;
+    final Dependencies mDeps;
+
+    public static class Dependencies {
+        public void makeIpClient(Context context, String iface, IpClientCallbacks callbacks) {
+            IpClientUtil.makeIpClient(context, iface, callbacks);
+        }
+
+        public EthernetNetworkAgent makeEthernetNetworkAgent(Context context, Looper looper,
+                NetworkCapabilities nc, LinkProperties lp, int networkScore,
+                NetworkAgentConfig config, NetworkProvider provider,
+                EthernetNetworkAgent.Callbacks cb) {
+            return new EthernetNetworkAgent(context, looper, nc, lp, networkScore, config, provider,
+                    cb);
+        }
+
+        public InterfaceParams getNetworkInterfaceByName(String name) {
+            return InterfaceParams.getByName(name);
+        }
+    }
 
     public static class ConfigurationException extends AndroidRuntimeException {
         public ConfigurationException(String msg) {
@@ -77,10 +98,17 @@
     }
 
     public EthernetNetworkFactory(Handler handler, Context context, NetworkCapabilities filter) {
+        this(handler, context, filter, new Dependencies());
+    }
+
+    @VisibleForTesting
+    EthernetNetworkFactory(Handler handler, Context context, NetworkCapabilities filter,
+            Dependencies deps) {
         super(handler.getLooper(), context, NETWORK_TYPE, filter);
 
         mHandler = handler;
         mContext = context;
+        mDeps = deps;
 
         setScoreFilter(NETWORK_SCORE);
     }
@@ -149,7 +177,7 @@
         }
 
         NetworkInterfaceState iface = new NetworkInterfaceState(
-                ifaceName, hwAddress, mHandler, mContext, capabilities, this);
+                ifaceName, hwAddress, mHandler, mContext, capabilities, this, mDeps);
         iface.setIpConfig(ipConfiguration);
         mTrackingInterfaces.put(ifaceName, iface);
 
@@ -251,6 +279,7 @@
         private final Handler mHandler;
         private final Context mContext;
         private final NetworkFactory mNetworkFactory;
+        private final Dependencies mDeps;
         private final int mLegacyType;
 
         private static String sTcpBufferSizes = null;  // Lazy initialized.
@@ -260,7 +289,7 @@
 
         private volatile @Nullable IIpClient mIpClient;
         private @Nullable IpClientCallbacksImpl mIpClientCallback;
-        private @Nullable NetworkAgent mNetworkAgent;
+        private @Nullable EthernetNetworkAgent mNetworkAgent;
         private @Nullable IpConfiguration mIpConfig;
 
         /**
@@ -360,12 +389,14 @@
         }
 
         NetworkInterfaceState(String ifaceName, String hwAddress, Handler handler, Context context,
-                @NonNull NetworkCapabilities capabilities, NetworkFactory networkFactory) {
+                @NonNull NetworkCapabilities capabilities, NetworkFactory networkFactory,
+                Dependencies deps) {
             name = ifaceName;
             mCapabilities = checkNotNull(capabilities);
             mHandler = handler;
             mContext = context;
             mNetworkFactory = networkFactory;
+            mDeps = deps;
             int legacyType = ConnectivityManager.TYPE_NONE;
             int[] transportTypes = mCapabilities.getTransportTypes();
 
@@ -451,7 +482,7 @@
             }
 
             mIpClientCallback = new IpClientCallbacksImpl();
-            IpClientUtil.makeIpClient(mContext, name, mIpClientCallback);
+            mDeps.makeIpClient(mContext, name, mIpClientCallback);
             mIpClientCallback.awaitIpClientStart();
             if (sTcpBufferSizes == null) {
                 sTcpBufferSizes = mContext.getResources().getString(
@@ -474,18 +505,19 @@
                     .setLegacyTypeName(NETWORK_TYPE)
                     .setLegacyExtraInfo(mHwAddress)
                     .build();
-            mNetworkAgent = new NetworkAgent(mContext, mHandler.getLooper(),
-                    NETWORK_TYPE, mCapabilities, mLinkProperties,
-                    getNetworkScore(), config, mNetworkFactory.getProvider()) {
-                public void unwanted() {
-                    if (this == mNetworkAgent) {
-                        stop();
-                    } else if (mNetworkAgent != null) {
-                        Log.d(TAG, "Ignoring unwanted as we have a more modern " +
-                                "instance");
-                    }  // Otherwise, we've already called stop.
-                }
-            };
+            mNetworkAgent = mDeps.makeEthernetNetworkAgent(mContext, mHandler.getLooper(),
+                    mCapabilities, mLinkProperties, getNetworkScore(), config,
+                    mNetworkFactory.getProvider(), new EthernetNetworkAgent.Callbacks() {
+                        @Override
+                        public void onNetworkUnwanted() {
+                            if (this == mNetworkAgent.getCallbacks()) {
+                                stop();
+                            } else if (mNetworkAgent != null) {
+                                Log.d(TAG, "Ignoring unwanted as we have a more modern " +
+                                        "instance");
+                            }  // Otherwise, we've already called stop.
+                        }
+                    });
             mNetworkAgent.register();
             mNetworkAgent.markConnected();
         }
@@ -496,7 +528,7 @@
             stop();
             // If the interface has disappeared provisioning will fail over and over again, so
             // there is no point in starting again
-            if (null != InterfaceParams.getByName(name)) {
+            if (null != mDeps.getNetworkInterfaceByName(name)) {
                 start();
             }
         }
@@ -504,7 +536,7 @@
         void updateLinkProperties(LinkProperties linkProperties) {
             mLinkProperties = linkProperties;
             if (mNetworkAgent != null) {
-                mNetworkAgent.sendLinkProperties(linkProperties);
+                mNetworkAgent.sendLinkPropertiesImpl(linkProperties);
             }
         }
 
@@ -545,7 +577,7 @@
                         mLinkProperties);
             }
             mNetworkAgent.sendNetworkCapabilities(mCapabilities);
-            mNetworkAgent.sendLinkProperties(mLinkProperties);
+            mNetworkAgent.sendLinkPropertiesImpl(mLinkProperties);
 
             // As a note, getNetworkScore() is fairly expensive to calculate. This is fine for now
             // since the agent isn't updated frequently. Consider caching the score in the future if
diff --git a/tests/ethernet/Android.bp b/tests/ethernet/Android.bp
index 4b2d270..1bc9352 100644
--- a/tests/ethernet/Android.bp
+++ b/tests/ethernet/Android.bp
@@ -28,10 +28,14 @@
     libs: [
         "android.test.runner",
         "android.test.base",
+        "android.test.mock",
     ],
 
     static_libs: [
         "androidx.test.rules",
         "ethernet-service",
+        "frameworks-base-testutils",
+        "mockito-target-minus-junit4",
+        "services.net",
     ],
 }
diff --git a/tests/ethernet/AndroidManifest.xml b/tests/ethernet/AndroidManifest.xml
index 302bb6c..cd875b0 100644
--- a/tests/ethernet/AndroidManifest.xml
+++ b/tests/ethernet/AndroidManifest.xml
@@ -18,7 +18,7 @@
 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.server.ethernet.tests">
 
-    <application android:label="EthernetServiceTests">
+    <application>
         <uses-library android:name="android.test.runner" />
     </application>
 
diff --git a/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java b/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
new file mode 100644
index 0000000..cc03ff2
--- /dev/null
+++ b/tests/ethernet/java/com/android/server/ethernet/EthernetNetworkFactoryTest.java
@@ -0,0 +1,395 @@
+/*
+ * 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 org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+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.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.test.MockAnswerUtil.AnswerWithArguments;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.EthernetNetworkSpecifier;
+import android.net.IpConfiguration;
+import android.net.LinkProperties;
+import android.net.NetworkAgentConfig;
+import android.net.NetworkCapabilities;
+import android.net.NetworkProvider;
+import android.net.NetworkRequest;
+import android.net.ip.IIpClient;
+import android.net.ip.IpClientCallbacks;
+import android.net.util.InterfaceParams;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.test.TestLooper;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+
+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;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class EthernetNetworkFactoryTest {
+    private TestLooper mLooper = new TestLooper();
+    private Handler mHandler;
+    private EthernetNetworkFactory mNetFactory = null;
+    private IpClientCallbacks mIpClientCallbacks;
+    private int mNetworkRequestCount = 0;
+    @Mock private Context mContext;
+    @Mock private Resources mResources;
+    @Mock private EthernetNetworkFactory.Dependencies mDeps;
+    @Mock private IIpClient mIpClient;
+    @Mock private EthernetNetworkAgent mNetworkAgent;
+    @Mock private InterfaceParams mInterfaceParams;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+
+        mHandler = new Handler(mLooper.getLooper());
+        mNetworkRequestCount = 0;
+
+        mNetFactory = new EthernetNetworkFactory(mHandler, mContext, createDefaultFilterCaps(),
+                mDeps);
+
+        setupNetworkAgentMock();
+        setupIpClientMock();
+        setupContext();
+    }
+
+    private void setupNetworkAgentMock() {
+        when(mDeps.makeEthernetNetworkAgent(any(), any(), any(), any(), anyInt(), any(), any(),
+                any())).thenAnswer(new AnswerWithArguments() {
+                                       public EthernetNetworkAgent answer(
+                                               Context context,
+                                               Looper looper,
+                                               NetworkCapabilities nc,
+                                               LinkProperties lp,
+                                               int networkScore,
+                                               NetworkAgentConfig config,
+                                               NetworkProvider provider,
+                                               EthernetNetworkAgent.Callbacks cb) {
+                                           when(mNetworkAgent.getCallbacks()).thenReturn(cb);
+                                           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(mIpClient);
+            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();
+    }
+
+    private void setupContext() {
+        when(mContext.getResources()).thenReturn(mResources);
+        when(mResources.getString(R.string.config_ethernet_tcp_buffers)).thenReturn(
+                "524288,1048576,3145728,524288,1048576,2097152");
+    }
+
+    @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() {
+        return new NetworkCapabilities.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .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;
+    }
+
+    // creates an interface with provisioning in progress (since updating the interface link state
+    // automatically starts the provisioning process)
+    private void createInterfaceUndergoingProvisioning(String iface) throws Exception {
+        mNetFactory.addInterface(iface, iface, createInterfaceCapsBuilder().build(),
+                createDefaultIpConfig());
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, true));
+        verify(mDeps).makeIpClient(any(Context.class), anyString(), any());
+        verify(mIpClient).startProvisioning(any());
+        clearInvocations(mDeps);
+        clearInvocations(mIpClient);
+    }
+
+    // creates a provisioned interface
+    private void createProvisionedInterface(String iface) throws Exception {
+        createInterfaceUndergoingProvisioning(iface);
+        mIpClientCallbacks.onProvisioningSuccess(new LinkProperties());
+        mLooper.dispatchAll();
+        // provisioning succeeded, verify that the network agent is created, registered, and marked
+        // as connected.
+        verify(mDeps).makeEthernetNetworkAgent(any(), any(), any(), any(), anyInt(), any(), any(),
+                any());
+        verify(mNetworkAgent).register();
+        verify(mNetworkAgent).markConnected();
+        clearInvocations(mDeps);
+        clearInvocations(mNetworkAgent);
+    }
+
+    // creates an unprovisioned interface
+    private void createUnprovisionedInterface(String iface) throws Exception {
+        // the only way to create an unprovisioned interface is by calling needNetworkFor
+        // followed by releaseNetworkFor which will stop the NetworkAgent and IpClient. When
+        // EthernetNetworkFactory#updateInterfaceLinkState(iface, true) is called, the interface
+        // is automatically provisioned even if nobody has ever called needNetworkFor
+        createProvisionedInterface(iface);
+
+        // Interface is already provisioned, so startProvisioning / register should not be called
+        // again
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient, never()).startProvisioning(any());
+        verify(mNetworkAgent, never()).register();
+
+        mNetFactory.releaseNetworkFor(createDefaultRequest());
+        verify(mIpClient).shutdown();
+        verify(mNetworkAgent).unregister();
+
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+    }
+
+    @Test
+    public void testAcceptRequest() throws Exception {
+        createInterfaceUndergoingProvisioning("eth0");
+        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 {
+        String iface = "eth0";
+        createInterfaceUndergoingProvisioning(iface);
+        // verify that the IpClient gets shut down when interface state changes to down.
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, false));
+        verify(mIpClient).shutdown();
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForProvisionedInterface() throws Exception {
+        String iface = "eth0";
+        createProvisionedInterface(iface);
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, false));
+        verify(mIpClient).shutdown();
+        verify(mNetworkAgent).unregister();
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForUnprovisionedInterface() throws Exception {
+        String iface = "eth0";
+        createUnprovisionedInterface(iface);
+        assertTrue(mNetFactory.updateInterfaceLinkState(iface, false));
+        // There should not be an active IPClient or NetworkAgent.
+        verify(mDeps, never()).makeIpClient(any(), any(), any());
+        verify(mDeps, never()).makeEthernetNetworkAgent(any(), any(), any(), any(), anyInt(), any(),
+            any(), any());
+    }
+
+    @Test
+    public void testUpdateInterfaceLinkStateForNonExistingInterface() throws Exception {
+        // if interface was never added, link state cannot be updated.
+        assertFalse(mNetFactory.updateInterfaceLinkState("eth1", true));
+        verify(mDeps, never()).makeIpClient(any(), any(), any());
+    }
+
+    @Test
+    public void testNeedNetworkForOnProvisionedInterface() throws Exception {
+        createProvisionedInterface("eth0");
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient, never()).startProvisioning(any());
+    }
+
+    @Test
+    public void testNeedNetworkForOnUnprovisionedInterface() throws Exception {
+        createUnprovisionedInterface("eth0");
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient).startProvisioning(any());
+
+        mIpClientCallbacks.onProvisioningSuccess(new LinkProperties());
+        mLooper.dispatchAll();
+        verify(mNetworkAgent).register();
+        verify(mNetworkAgent).markConnected();
+    }
+
+    @Test
+    public void testNeedNetworkForOnInterfaceUndergoingProvisioning() throws Exception {
+        createInterfaceUndergoingProvisioning("eth0");
+        mNetFactory.needNetworkFor(createDefaultRequest());
+        verify(mIpClient, never()).startProvisioning(any());
+
+        mIpClientCallbacks.onProvisioningSuccess(new LinkProperties());
+        mLooper.dispatchAll();
+        verify(mNetworkAgent).register();
+        verify(mNetworkAgent).markConnected();
+    }
+
+    @Test
+    public void testProvisioningLoss() throws Exception {
+        String iface = "eth0";
+        when(mDeps.getNetworkInterfaceByName(iface)).thenReturn(mInterfaceParams);
+        createProvisionedInterface(iface);
+
+        mIpClientCallbacks.onProvisioningFailure(new LinkProperties());
+        mLooper.dispatchAll();
+        verify(mIpClient).shutdown();
+        verify(mNetworkAgent).unregister();
+        // provisioning loss should trigger a retry, since the interface is still there
+        verify(mIpClient).startProvisioning(any());
+    }
+
+    @Test
+    public void testProvisioningLossForDisappearedInterface() throws Exception {
+        String iface = "eth0";
+        // mocked method returns null by default, but just to be explicit in the test:
+        when(mDeps.getNetworkInterfaceByName(eq(iface))).thenReturn(null);
+
+        createProvisionedInterface(iface);
+        mIpClientCallbacks.onProvisioningFailure(new LinkProperties());
+        mLooper.dispatchAll();
+        verify(mIpClient).shutdown();
+        verify(mNetworkAgent).unregister();
+        // the interface disappeared and getNetworkInterfaceByName returns null, we should not retry
+        verify(mIpClient, never()).startProvisioning(any());
+    }
+
+    @Test
+    public void testIpClientIsNotStartedWhenLinkIsDown() throws Exception {
+        String iface = "eth0";
+        createUnprovisionedInterface(iface);
+        mNetFactory.updateInterfaceLinkState(iface, false);
+
+        mNetFactory.needNetworkFor(createDefaultRequest());
+
+        NetworkRequest specificNetRequest = new NetworkRequest.Builder()
+                .addTransportType(NetworkCapabilities.TRANSPORT_ETHERNET)
+                .setNetworkSpecifier(new EthernetNetworkSpecifier(iface))
+                .build();
+        mNetFactory.needNetworkFor(specificNetRequest);
+
+        // TODO(b/155707957): BUG: IPClient should not be started when the interface link state
+        //  is down.
+        verify(mDeps).makeIpClient(any(), any(), any());
+    }
+
+    @Test
+    public void testLinkPropertiesChanged() throws Exception {
+        createProvisionedInterface("eth0");
+
+        LinkProperties lp = new LinkProperties();
+        mIpClientCallbacks.onLinkPropertiesChange(lp);
+        mLooper.dispatchAll();
+        verify(mNetworkAgent).sendLinkPropertiesImpl(same(lp));
+    }
+
+    @Test
+    public void testNetworkUnwanted() throws Exception {
+        createProvisionedInterface("eth0");
+
+        mNetworkAgent.getCallbacks().onNetworkUnwanted();
+        mLooper.dispatchAll();
+        verify(mIpClient).shutdown();
+        verify(mNetworkAgent).unregister();
+    }
+
+    @Test
+    public void testNetworkUnwantedWithStaleNetworkAgent() throws Exception {
+        String iface = "eth0";
+        // ensures provisioning is restarted after provisioning loss
+        when(mDeps.getNetworkInterfaceByName(iface)).thenReturn(mInterfaceParams);
+        createProvisionedInterface(iface);
+
+        EthernetNetworkAgent.Callbacks oldCbs = mNetworkAgent.getCallbacks();
+        // replace network agent in EthernetNetworkFactory
+        // Loss of provisioning will restart the ip client and network agent.
+        mIpClientCallbacks.onProvisioningFailure(new LinkProperties());
+        mLooper.dispatchAll();
+        verify(mDeps).makeIpClient(any(), any(), any());
+
+        mIpClientCallbacks.onProvisioningSuccess(new LinkProperties());
+        mLooper.dispatchAll();
+        verify(mDeps).makeEthernetNetworkAgent(any(), any(), any(), any(), anyInt(), any(), any(),
+                any());
+
+        // verify that unwanted is ignored
+        clearInvocations(mIpClient);
+        clearInvocations(mNetworkAgent);
+        oldCbs.onNetworkUnwanted();
+        verify(mIpClient, never()).shutdown();
+        verify(mNetworkAgent, never()).unregister();
+    }
+}