| /* |
| * Copyright (C) 2016 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.connectivity.tethering; |
| |
| import static org.mockito.Matchers.anyString; |
| import static org.mockito.Mockito.doThrow; |
| import static org.mockito.Mockito.inOrder; |
| import static org.mockito.Mockito.reset; |
| import static org.mockito.Mockito.verify; |
| import static org.mockito.Mockito.verifyNoMoreInteractions; |
| import static org.mockito.Mockito.when; |
| |
| import static android.net.ConnectivityManager.TETHER_ERROR_ENABLE_NAT_ERROR; |
| import static android.net.ConnectivityManager.TETHER_ERROR_NO_ERROR; |
| import static android.net.ConnectivityManager.TETHER_ERROR_TETHER_IFACE_ERROR; |
| import static android.net.ConnectivityManager.TETHER_ERROR_UNTETHER_IFACE_ERROR; |
| import static com.android.server.connectivity.tethering.IControlsTethering.STATE_AVAILABLE; |
| import static com.android.server.connectivity.tethering.IControlsTethering.STATE_TETHERED; |
| import static com.android.server.connectivity.tethering.IControlsTethering.STATE_UNAVAILABLE; |
| |
| import android.net.ConnectivityManager; |
| import android.net.INetworkStatsService; |
| import android.net.InterfaceConfiguration; |
| import android.os.INetworkManagementService; |
| import android.os.RemoteException; |
| import android.os.test.TestLooper; |
| import android.support.test.filters.SmallTest; |
| import android.support.test.runner.AndroidJUnit4; |
| |
| import org.junit.Before; |
| import org.junit.Test; |
| import org.junit.runner.RunWith; |
| import org.mockito.InOrder; |
| import org.mockito.Mock; |
| import org.mockito.MockitoAnnotations; |
| |
| @RunWith(AndroidJUnit4.class) |
| @SmallTest |
| public class TetherInterfaceStateMachineTest { |
| private static final String IFACE_NAME = "testnet1"; |
| private static final String UPSTREAM_IFACE = "upstream0"; |
| private static final String UPSTREAM_IFACE2 = "upstream1"; |
| |
| @Mock private INetworkManagementService mNMService; |
| @Mock private INetworkStatsService mStatsService; |
| @Mock private IControlsTethering mTetherHelper; |
| @Mock private InterfaceConfiguration mInterfaceConfiguration; |
| |
| private final TestLooper mLooper = new TestLooper(); |
| private TetherInterfaceStateMachine mTestedSm; |
| |
| private void initStateMachine(int interfaceType) throws Exception { |
| mTestedSm = new TetherInterfaceStateMachine(IFACE_NAME, mLooper.getLooper(), interfaceType, |
| mNMService, mStatsService, mTetherHelper); |
| mTestedSm.start(); |
| // Starting the state machine always puts us in a consistent state and notifies |
| // the test of the world that we've changed from an unknown to available state. |
| mLooper.dispatchAll(); |
| reset(mNMService, mStatsService, mTetherHelper); |
| when(mNMService.getInterfaceConfig(IFACE_NAME)).thenReturn(mInterfaceConfiguration); |
| } |
| |
| private void initTetheredStateMachine(int interfaceType, String upstreamIface) throws Exception { |
| initStateMachine(interfaceType); |
| dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED); |
| if (upstreamIface != null) { |
| dispatchTetherConnectionChanged(upstreamIface); |
| } |
| reset(mNMService, mStatsService, mTetherHelper); |
| when(mNMService.getInterfaceConfig(IFACE_NAME)).thenReturn(mInterfaceConfiguration); |
| } |
| |
| @Before public void setUp() throws Exception { |
| MockitoAnnotations.initMocks(this); |
| } |
| |
| @Test |
| public void startsOutAvailable() { |
| mTestedSm = new TetherInterfaceStateMachine(IFACE_NAME, mLooper.getLooper(), |
| ConnectivityManager.TETHERING_BLUETOOTH, mNMService, mStatsService, mTetherHelper); |
| mTestedSm.start(); |
| mLooper.dispatchAll(); |
| verify(mTetherHelper).notifyInterfaceStateChange( |
| IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR); |
| verifyNoMoreInteractions(mTetherHelper, mNMService, mStatsService); |
| } |
| |
| @Test |
| public void shouldDoNothingUntilRequested() throws Exception { |
| initStateMachine(ConnectivityManager.TETHERING_BLUETOOTH); |
| final int [] NOOP_COMMANDS = { |
| TetherInterfaceStateMachine.CMD_TETHER_UNREQUESTED, |
| TetherInterfaceStateMachine.CMD_IP_FORWARDING_ENABLE_ERROR, |
| TetherInterfaceStateMachine.CMD_IP_FORWARDING_DISABLE_ERROR, |
| TetherInterfaceStateMachine.CMD_START_TETHERING_ERROR, |
| TetherInterfaceStateMachine.CMD_STOP_TETHERING_ERROR, |
| TetherInterfaceStateMachine.CMD_SET_DNS_FORWARDERS_ERROR, |
| TetherInterfaceStateMachine.CMD_TETHER_CONNECTION_CHANGED |
| }; |
| for (int command : NOOP_COMMANDS) { |
| // None of these commands should trigger us to request action from |
| // the rest of the system. |
| dispatchCommand(command); |
| verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper); |
| } |
| } |
| |
| @Test |
| public void handlesImmediateInterfaceDown() throws Exception { |
| initStateMachine(ConnectivityManager.TETHERING_BLUETOOTH); |
| |
| dispatchCommand(TetherInterfaceStateMachine.CMD_INTERFACE_DOWN); |
| verify(mTetherHelper).notifyInterfaceStateChange( |
| IFACE_NAME, mTestedSm, STATE_UNAVAILABLE, TETHER_ERROR_NO_ERROR); |
| verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper); |
| } |
| |
| @Test |
| public void canBeTethered() throws Exception { |
| initStateMachine(ConnectivityManager.TETHERING_BLUETOOTH); |
| |
| dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED); |
| InOrder inOrder = inOrder(mTetherHelper, mNMService); |
| inOrder.verify(mNMService).tetherInterface(IFACE_NAME); |
| inOrder.verify(mTetherHelper).notifyInterfaceStateChange( |
| IFACE_NAME, mTestedSm, STATE_TETHERED, TETHER_ERROR_NO_ERROR); |
| verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper); |
| } |
| |
| @Test |
| public void canUnrequestTethering() throws Exception { |
| initTetheredStateMachine(ConnectivityManager.TETHERING_BLUETOOTH, null); |
| |
| dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_UNREQUESTED); |
| InOrder inOrder = inOrder(mNMService, mStatsService, mTetherHelper); |
| inOrder.verify(mNMService).untetherInterface(IFACE_NAME); |
| inOrder.verify(mTetherHelper).notifyInterfaceStateChange( |
| IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR); |
| verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper); |
| } |
| |
| @Test |
| public void canBeTetheredAsUsb() throws Exception { |
| initStateMachine(ConnectivityManager.TETHERING_USB); |
| |
| dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED); |
| InOrder inOrder = inOrder(mTetherHelper, mNMService); |
| inOrder.verify(mNMService).getInterfaceConfig(IFACE_NAME); |
| inOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration); |
| inOrder.verify(mNMService).tetherInterface(IFACE_NAME); |
| inOrder.verify(mTetherHelper).notifyInterfaceStateChange( |
| IFACE_NAME, mTestedSm, STATE_TETHERED, TETHER_ERROR_NO_ERROR); |
| verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper); |
| } |
| |
| @Test |
| public void handlesFirstUpstreamChange() throws Exception { |
| initTetheredStateMachine(ConnectivityManager.TETHERING_BLUETOOTH, null); |
| |
| // Telling the state machine about its upstream interface triggers a little more configuration. |
| dispatchTetherConnectionChanged(UPSTREAM_IFACE); |
| InOrder inOrder = inOrder(mNMService); |
| inOrder.verify(mNMService).enableNat(IFACE_NAME, UPSTREAM_IFACE); |
| inOrder.verify(mNMService).startInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE); |
| verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper); |
| } |
| |
| @Test |
| public void handlesChangingUpstream() throws Exception { |
| initTetheredStateMachine(ConnectivityManager.TETHERING_BLUETOOTH, UPSTREAM_IFACE); |
| |
| dispatchTetherConnectionChanged(UPSTREAM_IFACE2); |
| InOrder inOrder = inOrder(mNMService, mStatsService); |
| inOrder.verify(mStatsService).forceUpdate(); |
| inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE); |
| inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE); |
| inOrder.verify(mNMService).enableNat(IFACE_NAME, UPSTREAM_IFACE2); |
| inOrder.verify(mNMService).startInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE2); |
| verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper); |
| } |
| |
| @Test |
| public void canUnrequestTetheringWithUpstream() throws Exception { |
| initTetheredStateMachine(ConnectivityManager.TETHERING_BLUETOOTH, UPSTREAM_IFACE); |
| |
| dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_UNREQUESTED); |
| InOrder inOrder = inOrder(mNMService, mStatsService, mTetherHelper); |
| inOrder.verify(mStatsService).forceUpdate(); |
| inOrder.verify(mNMService).stopInterfaceForwarding(IFACE_NAME, UPSTREAM_IFACE); |
| inOrder.verify(mNMService).disableNat(IFACE_NAME, UPSTREAM_IFACE); |
| inOrder.verify(mNMService).untetherInterface(IFACE_NAME); |
| inOrder.verify(mTetherHelper).notifyInterfaceStateChange( |
| IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_NO_ERROR); |
| verifyNoMoreInteractions(mNMService, mStatsService, mTetherHelper); |
| } |
| |
| @Test |
| public void interfaceDownLeadsToUnavailable() throws Exception { |
| for (boolean shouldThrow : new boolean[]{true, false}) { |
| initTetheredStateMachine(ConnectivityManager.TETHERING_USB, null); |
| |
| if (shouldThrow) { |
| doThrow(RemoteException.class).when(mNMService).untetherInterface(IFACE_NAME); |
| } |
| dispatchCommand(TetherInterfaceStateMachine.CMD_INTERFACE_DOWN); |
| InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration, mTetherHelper); |
| usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown(); |
| usbTeardownOrder.verify(mNMService).setInterfaceConfig( |
| IFACE_NAME, mInterfaceConfiguration); |
| usbTeardownOrder.verify(mTetherHelper).notifyInterfaceStateChange( |
| IFACE_NAME, mTestedSm, STATE_UNAVAILABLE, TETHER_ERROR_NO_ERROR); |
| } |
| } |
| |
| @Test |
| public void usbShouldBeTornDownOnTetherError() throws Exception { |
| initStateMachine(ConnectivityManager.TETHERING_USB); |
| |
| doThrow(RemoteException.class).when(mNMService).tetherInterface(IFACE_NAME); |
| dispatchCommand(TetherInterfaceStateMachine.CMD_TETHER_REQUESTED); |
| InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration, mTetherHelper); |
| usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown(); |
| usbTeardownOrder.verify(mNMService).setInterfaceConfig( |
| IFACE_NAME, mInterfaceConfiguration); |
| usbTeardownOrder.verify(mTetherHelper).notifyInterfaceStateChange( |
| IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_TETHER_IFACE_ERROR); |
| } |
| |
| @Test |
| public void shouldTearDownUsbOnUpstreamError() throws Exception { |
| initTetheredStateMachine(ConnectivityManager.TETHERING_USB, null); |
| |
| doThrow(RemoteException.class).when(mNMService).enableNat(anyString(), anyString()); |
| dispatchTetherConnectionChanged(UPSTREAM_IFACE); |
| InOrder usbTeardownOrder = inOrder(mNMService, mInterfaceConfiguration, mTetherHelper); |
| usbTeardownOrder.verify(mInterfaceConfiguration).setInterfaceDown(); |
| usbTeardownOrder.verify(mNMService).setInterfaceConfig(IFACE_NAME, mInterfaceConfiguration); |
| usbTeardownOrder.verify(mTetherHelper).notifyInterfaceStateChange( |
| IFACE_NAME, mTestedSm, STATE_AVAILABLE, TETHER_ERROR_ENABLE_NAT_ERROR); |
| } |
| |
| /** |
| * Send a command to the state machine under test, and run the event loop to idle. |
| * |
| * @param command One of the TetherInterfaceStateMachine.CMD_* constants. |
| */ |
| private void dispatchCommand(int command) { |
| mTestedSm.sendMessage(command); |
| mLooper.dispatchAll(); |
| } |
| |
| /** |
| * Special override to tell the state machine that the upstream interface has changed. |
| * |
| * @see #dispatchCommand(int) |
| * @param upstreamIface String name of upstream interface (or null) |
| */ |
| private void dispatchTetherConnectionChanged(String upstreamIface) { |
| mTestedSm.sendMessage(TetherInterfaceStateMachine.CMD_TETHER_CONNECTION_CHANGED, |
| upstreamIface); |
| mLooper.dispatchAll(); |
| } |
| } |