1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.network; 18 19 import static android.arch.lifecycle.Lifecycle.Event.ON_START; 20 import static android.arch.lifecycle.Lifecycle.Event.ON_STOP; 21 import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OFF; 22 import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC; 23 import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; 24 import static android.provider.Settings.Global.PRIVATE_DNS_DEFAULT_MODE; 25 import static android.provider.Settings.Global.PRIVATE_DNS_MODE; 26 import static android.provider.Settings.Global.PRIVATE_DNS_SPECIFIER; 27 import static com.google.common.truth.Truth.assertThat; 28 import static org.mockito.ArgumentMatchers.nullable; 29 import static org.mockito.Matchers.any; 30 import static org.mockito.Matchers.anyString; 31 import static org.mockito.Mockito.CALLS_REAL_METHODS; 32 import static org.mockito.Mockito.atLeastOnce; 33 import static org.mockito.Mockito.doNothing; 34 import static org.mockito.Mockito.mock; 35 import static org.mockito.Mockito.reset; 36 import static org.mockito.Mockito.spy; 37 import static org.mockito.Mockito.times; 38 import static org.mockito.Mockito.verify; 39 import static org.mockito.Mockito.withSettings; 40 import static org.mockito.Mockito.when; 41 42 import android.arch.lifecycle.LifecycleOwner; 43 import android.content.Context; 44 import android.content.ContentResolver; 45 import android.database.ContentObserver; 46 import android.net.ConnectivityManager; 47 import android.net.ConnectivityManager.NetworkCallback; 48 import android.net.LinkProperties; 49 import android.net.Network; 50 import android.os.Handler; 51 import android.provider.Settings; 52 import android.support.v7.preference.Preference; 53 import android.support.v7.preference.PreferenceScreen; 54 55 import com.android.settings.R; 56 import com.android.settings.testutils.SettingsRobolectricTestRunner; 57 import com.android.settingslib.core.lifecycle.Lifecycle; 58 59 import org.junit.Before; 60 import org.junit.Test; 61 import org.junit.runner.RunWith; 62 import org.mockito.ArgumentCaptor; 63 import org.mockito.Captor; 64 import org.mockito.Mock; 65 import org.mockito.MockitoAnnotations; 66 import org.robolectric.RuntimeEnvironment; 67 import org.robolectric.shadow.api.Shadow; 68 import org.robolectric.shadows.ShadowContentResolver; 69 import org.robolectric.shadows.ShadowServiceManager; 70 71 import java.net.InetAddress; 72 import java.net.UnknownHostException; 73 import java.util.Arrays; 74 import java.util.Collections; 75 import java.util.List; 76 77 @RunWith(SettingsRobolectricTestRunner.class) 78 public class PrivateDnsPreferenceControllerTest { 79 80 private final static String HOSTNAME = "dns.example.com"; 81 private final static List<InetAddress> NON_EMPTY_ADDRESS_LIST; 82 static { 83 try { 84 NON_EMPTY_ADDRESS_LIST = Arrays.asList( 85 InetAddress.getByAddress(new byte[] { 8, 8, 8, 8 })); 86 } catch (UnknownHostException e) { 87 throw new RuntimeException("Invalid hardcoded IP addresss: " + e); 88 } 89 } 90 91 @Mock 92 private PreferenceScreen mScreen; 93 @Mock 94 private ConnectivityManager mConnectivityManager; 95 @Mock 96 private Network mNetwork; 97 @Mock 98 private Preference mPreference; 99 @Captor 100 private ArgumentCaptor<NetworkCallback> mCallbackCaptor; 101 private PrivateDnsPreferenceController mController; 102 private Context mContext; 103 private ContentResolver mContentResolver; 104 private ShadowContentResolver mShadowContentResolver; 105 private Lifecycle mLifecycle; 106 private LifecycleOwner mLifecycleOwner; 107 108 @Before setUp()109 public void setUp() { 110 MockitoAnnotations.initMocks(this); 111 mContext = spy(RuntimeEnvironment.application); 112 mContentResolver = mContext.getContentResolver(); 113 mShadowContentResolver = Shadow.extract(mContentResolver); 114 when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)) 115 .thenReturn(mConnectivityManager); 116 doNothing().when(mConnectivityManager).registerDefaultNetworkCallback( 117 mCallbackCaptor.capture(), nullable(Handler.class)); 118 119 when(mScreen.findPreference(anyString())).thenReturn(mPreference); 120 121 mController = spy(new PrivateDnsPreferenceController(mContext)); 122 123 mLifecycleOwner = () -> mLifecycle; 124 mLifecycle = new Lifecycle(mLifecycleOwner); 125 mLifecycle.addObserver(mController); 126 } 127 updateLinkProperties(LinkProperties lp)128 private void updateLinkProperties(LinkProperties lp) { 129 NetworkCallback nc = mCallbackCaptor.getValue(); 130 // The network callback that has been captured by the captor is the `mNetworkCallback' 131 // member of mController. mController being a spy, it has copied that member from the 132 // original object it was spying on, which means the object returned by the captor 133 // has a reference to the original object instead of the mock as its outer instance 134 // and will call methods and modify members of the original object instead of the spy, 135 // so methods subsequently called on the spy will not be aware of the changes. To work 136 // around this, the following code will create a new instance of the same class with 137 // the same code, but it sets the spy as the outer instance. 138 // A more recent version of Mockito would have made possible to create the spy with 139 // spy(PrivateDnsPreferenceController.class, withSettings().useConstructor(mContext)) 140 // and that would have solved the problem by removing the original object entirely 141 // in a more elegant manner, but useConstructor(Object...) is only available starting 142 // with Mockito 2.7.14. Other solutions involve modifying the code under test for 143 // the sake of the test. 144 nc = mock(nc.getClass(), withSettings().useConstructor().outerInstance(mController) 145 .defaultAnswer(CALLS_REAL_METHODS)); 146 nc.onLinkPropertiesChanged(mNetwork, lp); 147 } 148 149 @Test goThroughLifecycle_shouldRegisterUnregisterSettingsObserver()150 public void goThroughLifecycle_shouldRegisterUnregisterSettingsObserver() { 151 mLifecycle.handleLifecycleEvent(ON_START); 152 verify(mContext, atLeastOnce()).getContentResolver(); 153 assertThat(mShadowContentResolver.getContentObservers( 154 Settings.Global.getUriFor(PRIVATE_DNS_MODE))).isNotEmpty(); 155 assertThat(mShadowContentResolver.getContentObservers( 156 Settings.Global.getUriFor(PRIVATE_DNS_SPECIFIER))).isNotEmpty(); 157 158 159 mLifecycle.handleLifecycleEvent(ON_STOP); 160 verify(mContext, atLeastOnce()).getContentResolver(); 161 assertThat(mShadowContentResolver.getContentObservers( 162 Settings.Global.getUriFor(PRIVATE_DNS_MODE))).isEmpty(); 163 assertThat(mShadowContentResolver.getContentObservers( 164 Settings.Global.getUriFor(PRIVATE_DNS_SPECIFIER))).isEmpty(); 165 } 166 167 @Test getSummary_PrivateDnsModeOff()168 public void getSummary_PrivateDnsModeOff() { 169 setPrivateDnsMode(PRIVATE_DNS_MODE_OFF); 170 setPrivateDnsProviderHostname(HOSTNAME); 171 mController.updateState(mPreference); 172 verify(mController, atLeastOnce()).getSummary(); 173 verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_off)); 174 } 175 176 @Test getSummary_PrivateDnsModeOpportunistic()177 public void getSummary_PrivateDnsModeOpportunistic() { 178 mLifecycle.handleLifecycleEvent(ON_START); 179 setPrivateDnsMode(PRIVATE_DNS_MODE_OPPORTUNISTIC); 180 setPrivateDnsProviderHostname(HOSTNAME); 181 mController.updateState(mPreference); 182 verify(mController, atLeastOnce()).getSummary(); 183 verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_opportunistic)); 184 185 LinkProperties lp = mock(LinkProperties.class); 186 when(lp.getValidatedPrivateDnsServers()).thenReturn(NON_EMPTY_ADDRESS_LIST); 187 updateLinkProperties(lp); 188 mController.updateState(mPreference); 189 verify(mPreference).setSummary(getResourceString(R.string.switch_on_text)); 190 191 reset(mPreference); 192 lp = mock(LinkProperties.class); 193 when(lp.getValidatedPrivateDnsServers()).thenReturn(Collections.emptyList()); 194 updateLinkProperties(lp); 195 mController.updateState(mPreference); 196 verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_opportunistic)); 197 } 198 199 @Test getSummary_PrivateDnsModeProviderHostname()200 public void getSummary_PrivateDnsModeProviderHostname() { 201 mLifecycle.handleLifecycleEvent(ON_START); 202 setPrivateDnsMode(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME); 203 setPrivateDnsProviderHostname(HOSTNAME); 204 mController.updateState(mPreference); 205 verify(mController, atLeastOnce()).getSummary(); 206 verify(mPreference).setSummary( 207 getResourceString(R.string.private_dns_mode_provider_failure)); 208 209 LinkProperties lp = mock(LinkProperties.class); 210 when(lp.getValidatedPrivateDnsServers()).thenReturn(NON_EMPTY_ADDRESS_LIST); 211 updateLinkProperties(lp); 212 mController.updateState(mPreference); 213 verify(mPreference).setSummary(HOSTNAME); 214 215 reset(mPreference); 216 lp = mock(LinkProperties.class); 217 when(lp.getValidatedPrivateDnsServers()).thenReturn(Collections.emptyList()); 218 updateLinkProperties(lp); 219 mController.updateState(mPreference); 220 verify(mPreference).setSummary( 221 getResourceString(R.string.private_dns_mode_provider_failure)); 222 } 223 224 @Test getSummary_PrivateDnsDefaultMode()225 public void getSummary_PrivateDnsDefaultMode() { 226 // Default mode is opportunistic, unless overridden by a Settings push. 227 setPrivateDnsMode(""); 228 setPrivateDnsProviderHostname(""); 229 mController.updateState(mPreference); 230 verify(mController, atLeastOnce()).getSummary(); 231 verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_opportunistic)); 232 233 reset(mController); 234 reset(mPreference); 235 // Pretend an emergency gservices setting has disabled default-opportunistic. 236 Settings.Global.putString(mContentResolver, PRIVATE_DNS_DEFAULT_MODE, PRIVATE_DNS_MODE_OFF); 237 mController.updateState(mPreference); 238 verify(mController, atLeastOnce()).getSummary(); 239 verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_off)); 240 241 reset(mController); 242 reset(mPreference); 243 // The user interacting with the Private DNS menu, explicitly choosing 244 // opportunistic mode, will be able to use despite the change to the 245 // default setting above. 246 setPrivateDnsMode(PRIVATE_DNS_MODE_OPPORTUNISTIC); 247 mController.updateState(mPreference); 248 verify(mController, atLeastOnce()).getSummary(); 249 verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_opportunistic)); 250 } 251 setPrivateDnsMode(String mode)252 private void setPrivateDnsMode(String mode) { 253 Settings.Global.putString(mContentResolver, PRIVATE_DNS_MODE, mode); 254 } 255 setPrivateDnsProviderHostname(String name)256 private void setPrivateDnsProviderHostname(String name) { 257 Settings.Global.putString(mContentResolver, PRIVATE_DNS_SPECIFIER, name); 258 } 259 getResourceString(int which)260 private String getResourceString(int which) { 261 return mContext.getResources().getString(which); 262 } 263 } 264