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.net.ConnectivityManager.PRIVATE_DNS_MODE_OFF; 20 import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC; 21 import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME; 22 import static android.provider.Settings.Global.PRIVATE_DNS_DEFAULT_MODE; 23 import static android.provider.Settings.Global.PRIVATE_DNS_MODE; 24 import static android.provider.Settings.Global.PRIVATE_DNS_SPECIFIER; 25 26 import static androidx.lifecycle.Lifecycle.Event.ON_START; 27 import static androidx.lifecycle.Lifecycle.Event.ON_STOP; 28 29 import static com.android.settings.core.BasePreferenceController.AVAILABLE; 30 import static com.android.settings.core.BasePreferenceController.UNSUPPORTED_ON_DEVICE; 31 32 import static com.google.common.truth.Truth.assertThat; 33 34 import static org.mockito.ArgumentMatchers.anyString; 35 import static org.mockito.ArgumentMatchers.nullable; 36 import static org.mockito.Mockito.CALLS_REAL_METHODS; 37 import static org.mockito.Mockito.atLeastOnce; 38 import static org.mockito.Mockito.doNothing; 39 import static org.mockito.Mockito.mock; 40 import static org.mockito.Mockito.reset; 41 import static org.mockito.Mockito.spy; 42 import static org.mockito.Mockito.verify; 43 import static org.mockito.Mockito.when; 44 import static org.mockito.Mockito.withSettings; 45 46 import android.content.ComponentName; 47 import android.content.ContentResolver; 48 import android.content.Context; 49 import android.net.ConnectivityManager; 50 import android.net.ConnectivityManager.NetworkCallback; 51 import android.net.LinkProperties; 52 import android.net.Network; 53 import android.os.Handler; 54 import android.os.UserHandle; 55 import android.os.UserManager; 56 import android.provider.Settings; 57 58 import androidx.lifecycle.LifecycleOwner; 59 import androidx.preference.Preference; 60 import androidx.preference.PreferenceScreen; 61 62 import com.android.settings.R; 63 import com.android.settings.testutils.shadow.ShadowUserManager; 64 import com.android.settings.testutils.shadow.ShadowDevicePolicyManager; 65 import com.android.settingslib.core.lifecycle.Lifecycle; 66 67 import org.junit.Before; 68 import org.junit.Test; 69 import org.junit.runner.RunWith; 70 import org.mockito.ArgumentCaptor; 71 import org.mockito.Captor; 72 import org.mockito.Mock; 73 import org.mockito.MockitoAnnotations; 74 import org.robolectric.RobolectricTestRunner; 75 import org.robolectric.RuntimeEnvironment; 76 import org.robolectric.annotation.Config; 77 import org.robolectric.shadow.api.Shadow; 78 import org.robolectric.shadows.ShadowContentResolver; 79 80 import java.net.InetAddress; 81 import java.net.UnknownHostException; 82 import java.util.Arrays; 83 import java.util.Collections; 84 import java.util.List; 85 86 @RunWith(RobolectricTestRunner.class) 87 @Config(shadows = { 88 ShadowUserManager.class, 89 ShadowDevicePolicyManager.class 90 }) 91 public class PrivateDnsPreferenceControllerTest { 92 93 private final static String HOSTNAME = "dns.example.com"; 94 private final static List<InetAddress> NON_EMPTY_ADDRESS_LIST; 95 static { 96 try { 97 NON_EMPTY_ADDRESS_LIST = Arrays.asList( 98 InetAddress.getByAddress(new byte[] { 8, 8, 8, 8 })); 99 } catch (UnknownHostException e) { 100 throw new RuntimeException("Invalid hardcoded IP addresss: " + e); 101 } 102 } 103 104 @Mock 105 private PreferenceScreen mScreen; 106 @Mock 107 private ConnectivityManager mConnectivityManager; 108 @Mock 109 private Network mNetwork; 110 @Mock 111 private Preference mPreference; 112 @Captor 113 private ArgumentCaptor<NetworkCallback> mCallbackCaptor; 114 private PrivateDnsPreferenceController mController; 115 private Context mContext; 116 private ContentResolver mContentResolver; 117 private ShadowContentResolver mShadowContentResolver; 118 private Lifecycle mLifecycle; 119 private LifecycleOwner mLifecycleOwner; 120 private ShadowUserManager mShadowUserManager; 121 122 @Before setUp()123 public void setUp() { 124 MockitoAnnotations.initMocks(this); 125 mContext = spy(RuntimeEnvironment.application); 126 mContentResolver = mContext.getContentResolver(); 127 mShadowContentResolver = Shadow.extract(mContentResolver); 128 when(mContext.getSystemService(Context.CONNECTIVITY_SERVICE)) 129 .thenReturn(mConnectivityManager); 130 doNothing().when(mConnectivityManager).registerDefaultNetworkCallback( 131 mCallbackCaptor.capture(), nullable(Handler.class)); 132 133 when(mScreen.findPreference(anyString())).thenReturn(mPreference); 134 135 mController = spy(new PrivateDnsPreferenceController(mContext)); 136 137 mLifecycleOwner = () -> mLifecycle; 138 mLifecycle = new Lifecycle(mLifecycleOwner); 139 mLifecycle.addObserver(mController); 140 141 mShadowUserManager = ShadowUserManager.getShadow(); 142 } 143 updateLinkProperties(LinkProperties lp)144 private void updateLinkProperties(LinkProperties lp) { 145 NetworkCallback nc = mCallbackCaptor.getValue(); 146 // The network callback that has been captured by the captor is the `mNetworkCallback' 147 // member of mController. mController being a spy, it has copied that member from the 148 // original object it was spying on, which means the object returned by the captor 149 // has a reference to the original object instead of the mock as its outer instance 150 // and will call methods and modify members of the original object instead of the spy, 151 // so methods subsequently called on the spy will not be aware of the changes. To work 152 // around this, the following code will create a new instance of the same class with 153 // the same code, but it sets the spy as the outer instance. 154 // A more recent version of Mockito would have made possible to create the spy with 155 // spy(PrivateDnsPreferenceController.class, withSettings().useConstructor(mContext)) 156 // and that would have solved the problem by removing the original object entirely 157 // in a more elegant manner, but useConstructor(Object...) is only available starting 158 // with Mockito 2.7.14. Other solutions involve modifying the code under test for 159 // the sake of the test. 160 nc = mock(nc.getClass(), withSettings().useConstructor().outerInstance(mController) 161 .defaultAnswer(CALLS_REAL_METHODS)); 162 nc.onLinkPropertiesChanged(mNetwork, lp); 163 } 164 165 @Test getAvailibilityStatus_availableByDefault()166 public void getAvailibilityStatus_availableByDefault() { 167 assertThat(mController.getAvailabilityStatus()).isEqualTo(AVAILABLE); 168 } 169 170 @Test 171 @Config(qualifiers = "mcc999") getAvailabilityStatus_unsupportedWhenSet()172 public void getAvailabilityStatus_unsupportedWhenSet() { 173 assertThat(mController.getAvailabilityStatus()).isEqualTo(UNSUPPORTED_ON_DEVICE); 174 } 175 176 @Test goThroughLifecycle_shouldRegisterUnregisterSettingsObserver()177 public void goThroughLifecycle_shouldRegisterUnregisterSettingsObserver() { 178 mLifecycle.handleLifecycleEvent(ON_START); 179 verify(mContext, atLeastOnce()).getContentResolver(); 180 assertThat(mShadowContentResolver.getContentObservers( 181 Settings.Global.getUriFor(PRIVATE_DNS_MODE))).isNotEmpty(); 182 assertThat(mShadowContentResolver.getContentObservers( 183 Settings.Global.getUriFor(PRIVATE_DNS_SPECIFIER))).isNotEmpty(); 184 185 186 mLifecycle.handleLifecycleEvent(ON_STOP); 187 verify(mContext, atLeastOnce()).getContentResolver(); 188 assertThat(mShadowContentResolver.getContentObservers( 189 Settings.Global.getUriFor(PRIVATE_DNS_MODE))).isEmpty(); 190 assertThat(mShadowContentResolver.getContentObservers( 191 Settings.Global.getUriFor(PRIVATE_DNS_SPECIFIER))).isEmpty(); 192 } 193 194 @Test getSummary_PrivateDnsModeOff()195 public void getSummary_PrivateDnsModeOff() { 196 setPrivateDnsMode(PRIVATE_DNS_MODE_OFF); 197 setPrivateDnsProviderHostname(HOSTNAME); 198 mController.updateState(mPreference); 199 verify(mController, atLeastOnce()).getSummary(); 200 verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_off)); 201 } 202 203 @Test getSummary_PrivateDnsModeOpportunistic()204 public void getSummary_PrivateDnsModeOpportunistic() { 205 mLifecycle.handleLifecycleEvent(ON_START); 206 setPrivateDnsMode(PRIVATE_DNS_MODE_OPPORTUNISTIC); 207 setPrivateDnsProviderHostname(HOSTNAME); 208 mController.updateState(mPreference); 209 verify(mController, atLeastOnce()).getSummary(); 210 verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_opportunistic)); 211 212 LinkProperties lp = mock(LinkProperties.class); 213 when(lp.getValidatedPrivateDnsServers()).thenReturn(NON_EMPTY_ADDRESS_LIST); 214 updateLinkProperties(lp); 215 mController.updateState(mPreference); 216 verify(mPreference).setSummary(getResourceString(R.string.switch_on_text)); 217 218 reset(mPreference); 219 lp = mock(LinkProperties.class); 220 when(lp.getValidatedPrivateDnsServers()).thenReturn(Collections.emptyList()); 221 updateLinkProperties(lp); 222 mController.updateState(mPreference); 223 verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_opportunistic)); 224 } 225 226 @Test getSummary_PrivateDnsModeProviderHostname()227 public void getSummary_PrivateDnsModeProviderHostname() { 228 mLifecycle.handleLifecycleEvent(ON_START); 229 setPrivateDnsMode(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME); 230 setPrivateDnsProviderHostname(HOSTNAME); 231 mController.updateState(mPreference); 232 verify(mController, atLeastOnce()).getSummary(); 233 verify(mPreference).setSummary( 234 getResourceString(R.string.private_dns_mode_provider_failure)); 235 236 LinkProperties lp = mock(LinkProperties.class); 237 when(lp.getValidatedPrivateDnsServers()).thenReturn(NON_EMPTY_ADDRESS_LIST); 238 updateLinkProperties(lp); 239 mController.updateState(mPreference); 240 verify(mPreference).setSummary(HOSTNAME); 241 242 reset(mPreference); 243 lp = mock(LinkProperties.class); 244 when(lp.getValidatedPrivateDnsServers()).thenReturn(Collections.emptyList()); 245 updateLinkProperties(lp); 246 mController.updateState(mPreference); 247 verify(mPreference).setSummary( 248 getResourceString(R.string.private_dns_mode_provider_failure)); 249 } 250 251 @Test getSummary_PrivateDnsDefaultMode()252 public void getSummary_PrivateDnsDefaultMode() { 253 // Default mode is opportunistic, unless overridden by a Settings push. 254 setPrivateDnsMode(""); 255 setPrivateDnsProviderHostname(""); 256 mController.updateState(mPreference); 257 verify(mController, atLeastOnce()).getSummary(); 258 verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_opportunistic)); 259 260 reset(mController); 261 reset(mPreference); 262 // Pretend an emergency gservices setting has disabled default-opportunistic. 263 Settings.Global.putString(mContentResolver, PRIVATE_DNS_DEFAULT_MODE, PRIVATE_DNS_MODE_OFF); 264 mController.updateState(mPreference); 265 verify(mController, atLeastOnce()).getSummary(); 266 verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_off)); 267 268 reset(mController); 269 reset(mPreference); 270 // The user interacting with the Private DNS menu, explicitly choosing 271 // opportunistic mode, will be able to use despite the change to the 272 // default setting above. 273 setPrivateDnsMode(PRIVATE_DNS_MODE_OPPORTUNISTIC); 274 mController.updateState(mPreference); 275 verify(mController, atLeastOnce()).getSummary(); 276 verify(mPreference).setSummary(getResourceString(R.string.private_dns_mode_opportunistic)); 277 } 278 279 @Test isEnabled_canBeDisabledByAdmin()280 public void isEnabled_canBeDisabledByAdmin() { 281 final int userId = UserHandle.myUserId(); 282 final List<UserManager.EnforcingUser> enforcingUsers = Collections.singletonList( 283 new UserManager.EnforcingUser(userId, 284 UserManager.RESTRICTION_SOURCE_DEVICE_OWNER) 285 ); 286 mShadowUserManager.setUserRestrictionSources( 287 UserManager.DISALLOW_CONFIG_PRIVATE_DNS, 288 UserHandle.of(userId), 289 enforcingUsers); 290 291 ShadowDevicePolicyManager.getShadow().setDeviceOwnerComponentOnAnyUser( 292 new ComponentName("test", "test")); 293 294 mController.updateState(mPreference); 295 verify(mPreference).setEnabled(false); 296 } 297 298 @Test isEnabled_isEnabledByDefault()299 public void isEnabled_isEnabledByDefault() { 300 mController.updateState(mPreference); 301 verify(mPreference).setEnabled(true); 302 } 303 setPrivateDnsMode(String mode)304 private void setPrivateDnsMode(String mode) { 305 Settings.Global.putString(mContentResolver, PRIVATE_DNS_MODE, mode); 306 } 307 setPrivateDnsProviderHostname(String name)308 private void setPrivateDnsProviderHostname(String name) { 309 Settings.Global.putString(mContentResolver, PRIVATE_DNS_SPECIFIER, name); 310 } 311 getResourceString(int which)312 private String getResourceString(int which) { 313 return mContext.getResources().getString(which); 314 } 315 } 316