|
39 | 39 | import android.app.Activity; |
40 | 40 | import android.content.ActivityNotFoundException; |
41 | 41 | import android.content.Context; |
| 42 | +import android.content.Intent; |
| 43 | +import android.content.pm.ActivityInfo; |
| 44 | +import android.content.pm.ResolveInfo; |
| 45 | +import android.net.Uri; |
42 | 46 | import android.net.http.SslError; |
43 | 47 | import android.webkit.SslErrorHandler; |
44 | 48 | import android.webkit.WebResourceError; |
|
74 | 78 | import org.mockito.Mockito; |
75 | 79 | import org.robolectric.Robolectric; |
76 | 80 | import org.robolectric.RobolectricTestRunner; |
| 81 | +import org.robolectric.Shadows; |
77 | 82 | import org.robolectric.annotation.Config; |
78 | | - |
| 83 | +import org.robolectric.shadows.ShadowPackageManager; |
79 | 84 | import java.util.HashMap; |
80 | 85 |
|
81 | 86 | import io.opentelemetry.api.trace.Span; |
@@ -128,6 +133,18 @@ public class AzureActiveDirectoryWebViewClientTest { |
128 | 133 | private static final String TEST_PLAYSTORE_REDIRECT_WITH_BROWSER_PROTOCOL = "browser://play.app.goo.gl/?link=https://play.google.com/store/apps/details?id=com.microsoft.windowsintune.companyportal"; |
129 | 134 | private static final String TEST_OPENID_VC_URL = "openid-vc://credential-offer?credential_issuer=https%3A%2F%2Fexample.com&credential_configuration_ids=VerifiedEmployee"; |
130 | 135 |
|
| 136 | + // Authenticator activation app link test URLs |
| 137 | + private static final String TEST_AUTHENTICATOR_ACTIVATION_GLOBAL = |
| 138 | + "https://login.microsoftonline.com/authenticatorApp/activateAccount?accountType=mfa&source=qrCode&accountType=msa&code=demo&uaid=0022d4c4141444b484dd38026d312794&expires=3971458484"; |
| 139 | + private static final String TEST_AUTHENTICATOR_ACTIVATION_CHINA = |
| 140 | + "https://login.chinacloudapi.cn/authenticatorApp/activateAccount?accountType=mfa&source=qrCode&accountType=msa&code=demo&uaid=0022d4c4141444b484dd38026d312794&expires=3971458484"; |
| 141 | + private static final String TEST_AUTHENTICATOR_ACTIVATION_US_GOV = |
| 142 | + "https://login.microsoftonline.us/authenticatorApp/activateAccount?accountType=mfa&source=qrCode&accountType=msa&code=demo&uaid=0022d4c4141444b484dd38026d312794&expires=3971458484"; |
| 143 | + private static final String TEST_AUTHENTICATOR_ACTIVATION_INVALID_HOST = |
| 144 | + "https://login.evil.com/authenticatorApp/activateAccount?accountType=mfa&code=123"; |
| 145 | + private static final String TEST_AUTHENTICATOR_ACTIVATION_INVALID_PATH = |
| 146 | + "https://login.microsoftonline.com/some/other/path?accountType=mfa&code=123"; |
| 147 | + |
131 | 148 | @Before |
132 | 149 | public void setup() throws ClientException { |
133 | 150 | mContext = ApplicationProvider.getApplicationContext(); |
@@ -922,4 +939,160 @@ public void testUrlTracker_onReceivedHttpError_subResource_doesNotCallUpdateLate |
922 | 939 |
|
923 | 940 | Mockito.verify(mockTracker, never()).updateLatestUrlStatus(any(), any()); |
924 | 941 | } |
| 942 | + |
| 943 | + // ===== Authenticator activation app link tests ===== |
| 944 | + |
| 945 | + @Test |
| 946 | + public void testIsAuthenticatorActivationAppLink_globalHost_shouldReturnTrue() { |
| 947 | + assertTrue(mWebViewClient.isAuthenticatorActivationAppLink( |
| 948 | + TEST_AUTHENTICATOR_ACTIVATION_GLOBAL.toLowerCase())); |
| 949 | + } |
| 950 | + |
| 951 | + @Test |
| 952 | + public void testIsAuthenticatorActivationAppLink_chinaHost_shouldReturnTrue() { |
| 953 | + assertTrue(mWebViewClient.isAuthenticatorActivationAppLink( |
| 954 | + TEST_AUTHENTICATOR_ACTIVATION_CHINA.toLowerCase())); |
| 955 | + } |
| 956 | + |
| 957 | + @Test |
| 958 | + public void testIsAuthenticatorActivationAppLink_usGovHost_shouldReturnTrue() { |
| 959 | + assertTrue(mWebViewClient.isAuthenticatorActivationAppLink( |
| 960 | + TEST_AUTHENTICATOR_ACTIVATION_US_GOV.toLowerCase())); |
| 961 | + } |
| 962 | + |
| 963 | + @Test |
| 964 | + public void testIsAuthenticatorActivationAppLink_invalidHost_shouldReturnFalse() { |
| 965 | + assertFalse(mWebViewClient.isAuthenticatorActivationAppLink( |
| 966 | + TEST_AUTHENTICATOR_ACTIVATION_INVALID_HOST.toLowerCase())); |
| 967 | + } |
| 968 | + |
| 969 | + @Test |
| 970 | + public void testIsAuthenticatorActivationAppLink_invalidPath_shouldReturnFalse() { |
| 971 | + assertFalse(mWebViewClient.isAuthenticatorActivationAppLink( |
| 972 | + TEST_AUTHENTICATOR_ACTIVATION_INVALID_PATH.toLowerCase())); |
| 973 | + } |
| 974 | + |
| 975 | + @Test |
| 976 | + public void testIsAuthenticatorActivationAppLink_nonHttpsScheme_shouldReturnFalse() { |
| 977 | + assertFalse(mWebViewClient.isAuthenticatorActivationAppLink( |
| 978 | + "http://login.microsoftonline.com/authenticatorapp/activateaccount")); |
| 979 | + } |
| 980 | + |
| 981 | + @Test |
| 982 | + public void testIsAuthenticatorActivationAppLink_legacyMfaScheme_shouldReturnFalse() { |
| 983 | + assertFalse(mWebViewClient.isAuthenticatorActivationAppLink( |
| 984 | + AUTHENTICATOR_MFA_LINKING_PREFIX.toLowerCase())); |
| 985 | + } |
| 986 | + |
| 987 | + /** |
| 988 | + * Registers a fake handler on the given activity's PackageManager so that |
| 989 | + * {@code intent.resolveActivity(...)} returns non-null for ACTION_VIEW + the given Uri. |
| 990 | + * This lets us exercise the "handler present" branch of |
| 991 | + * processAuthenticatorActivationAppLink in a Robolectric test. |
| 992 | + */ |
| 993 | + private void registerActivationHandler(@NonNull final Activity activity, |
| 994 | + @NonNull final Uri uri, |
| 995 | + @NonNull final String packageName, |
| 996 | + @NonNull final String activityClass) { |
| 997 | + final ShadowPackageManager shadowPm = Shadows.shadowOf(activity.getPackageManager()); |
| 998 | + final Intent intent = new Intent(Intent.ACTION_VIEW, uri); |
| 999 | + final ResolveInfo info = new ResolveInfo(); |
| 1000 | + info.activityInfo = new ActivityInfo(); |
| 1001 | + info.activityInfo.packageName = packageName; |
| 1002 | + info.activityInfo.name = activityClass; |
| 1003 | + shadowPm.addResolveInfoForIntent(intent, info); |
| 1004 | + } |
| 1005 | + |
| 1006 | + @Test |
| 1007 | + public void testAuthenticatorActivationAppLink_launchesIntent_whenHandlerPresent() { |
| 1008 | + // Arrange: register a handler so resolveActivity(...) returns non-null. |
| 1009 | + registerActivationHandler( |
| 1010 | + mActivity, |
| 1011 | + Uri.parse(TEST_AUTHENTICATOR_ACTIVATION_GLOBAL), |
| 1012 | + "com.azure.authenticator", |
| 1013 | + "com.azure.authenticator.ui.MainActivity"); |
| 1014 | + |
| 1015 | + final WebView mockWebView = Mockito.mock(WebView.class); |
| 1016 | + |
| 1017 | + // Act |
| 1018 | + final boolean handled = mWebViewClient.shouldOverrideUrlLoading( |
| 1019 | + mockWebView, TEST_AUTHENTICATOR_ACTIVATION_GLOBAL); |
| 1020 | + |
| 1021 | + // Assert: URL was intercepted, WebView stopped, and an ACTION_VIEW intent was launched. |
| 1022 | + assertTrue(handled); |
| 1023 | + Mockito.verify(mockWebView).stopLoading(); |
| 1024 | + |
| 1025 | + final Intent launched = Shadows.shadowOf(mActivity).getNextStartedActivity(); |
| 1026 | + Assert.assertNotNull("Expected an intent to be launched for the activation link", launched); |
| 1027 | + assertEquals(Intent.ACTION_VIEW, launched.getAction()); |
| 1028 | + assertEquals(TEST_AUTHENTICATOR_ACTIVATION_GLOBAL, launched.getData().toString()); |
| 1029 | + assertTrue("Expected FLAG_ACTIVITY_NEW_TASK on activation intent", |
| 1030 | + (launched.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0); |
| 1031 | + } |
| 1032 | + |
| 1033 | + @Test |
| 1034 | + public void testAuthenticatorActivationAppLink_noHandler_stopsLoading_andDoesNotCrash() { |
| 1035 | + // Arrange: no handler registered -> intent.resolveActivity() returns null in both |
| 1036 | + // the Authenticator launch path and the openLinkInBrowser fallback path. |
| 1037 | + final WebView mockWebView = Mockito.mock(WebView.class); |
| 1038 | + |
| 1039 | + // Act |
| 1040 | + final boolean handled = mWebViewClient.shouldOverrideUrlLoading( |
| 1041 | + mockWebView, TEST_AUTHENTICATOR_ACTIVATION_GLOBAL); |
| 1042 | + |
| 1043 | + // Assert: handled=true, WebView stopped, no activity started (no crash, no error callback). |
| 1044 | + assertTrue(handled); |
| 1045 | + Mockito.verify(mockWebView).stopLoading(); |
| 1046 | + Assert.assertNull("Expected NO intent to be launched when no handler is installed", |
| 1047 | + Shadows.shadowOf(mActivity).getNextStartedActivity()); |
| 1048 | + } |
| 1049 | + |
| 1050 | + @Test |
| 1051 | + public void testAuthenticatorActivationAppLink_preservesOriginalCasing() { |
| 1052 | + // The activation link carries case-sensitive query values (e.g. base64 codes). |
| 1053 | + // shouldOverrideUrlLoading lowercases the URL for matching but must dispatch the |
| 1054 | + // *original* URL to the Authenticator. |
| 1055 | + final String mixedCaseUrl = |
| 1056 | + "https://login.microsoftonline.com/authenticatorApp/activateAccount" |
| 1057 | + + "?accountType=mfa&source=QrCode&code=AbCdEf123XYZ&url=https://Service"; |
| 1058 | + registerActivationHandler( |
| 1059 | + mActivity, |
| 1060 | + Uri.parse(mixedCaseUrl), |
| 1061 | + "com.azure.authenticator", |
| 1062 | + "com.azure.authenticator.ui.MainActivity"); |
| 1063 | + |
| 1064 | + final WebView mockWebView = Mockito.mock(WebView.class); |
| 1065 | + |
| 1066 | + final boolean handled = mWebViewClient.shouldOverrideUrlLoading(mockWebView, mixedCaseUrl); |
| 1067 | + |
| 1068 | + assertTrue(handled); |
| 1069 | + final Intent launched = Shadows.shadowOf(mActivity).getNextStartedActivity(); |
| 1070 | + Assert.assertNotNull(launched); |
| 1071 | + assertEquals("Authenticator activation intent must carry the original-cased URL", |
| 1072 | + mixedCaseUrl, launched.getData().toString()); |
| 1073 | + } |
| 1074 | + |
| 1075 | + @Test |
| 1076 | + public void testProcessAuthAppMFAUrl_startsViewIntentWithNewTaskFlag() { |
| 1077 | + // microsoft-authenticator://activatemfa/... is handed to processAuthAppMFAUrl, |
| 1078 | + // which dispatches an ACTION_VIEW intent with FLAG_ACTIVITY_NEW_TASK. |
| 1079 | + final String mfaUrl = AUTHENTICATOR_MFA_LINKING_PREFIX + "/?x=1"; |
| 1080 | + // Make the intent resolvable so the OS would accept the launch. (Robolectric |
| 1081 | + // records the started activity regardless, but this documents the expectation.) |
| 1082 | + registerActivationHandler( |
| 1083 | + mActivity, |
| 1084 | + Uri.parse(mfaUrl), |
| 1085 | + "com.azure.authenticator", |
| 1086 | + "com.azure.authenticator.ui.MainActivity"); |
| 1087 | + |
| 1088 | + final boolean handled = mWebViewClient.shouldOverrideUrlLoading(mMockWebView, mfaUrl); |
| 1089 | + |
| 1090 | + assertTrue(handled); |
| 1091 | + final Intent launched = Shadows.shadowOf(mActivity).getNextStartedActivity(); |
| 1092 | + Assert.assertNotNull(launched); |
| 1093 | + assertEquals(Intent.ACTION_VIEW, launched.getAction()); |
| 1094 | + assertEquals(mfaUrl, launched.getDataString()); |
| 1095 | + assertTrue("MFA activation intent must carry FLAG_ACTIVITY_NEW_TASK", |
| 1096 | + (launched.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) != 0); |
| 1097 | + } |
925 | 1098 | } |
0 commit comments