1 /* 2 * Copyright (C) 2024 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 package com.android.credentialmanager 17 18 import java.time.Instant 19 import android.graphics.drawable.Drawable 20 import com.android.credentialmanager.model.get.CredentialEntryInfo 21 import com.android.credentialmanager.model.get.ActionEntryInfo 22 import com.android.credentialmanager.model.get.AuthenticationEntryInfo 23 import com.android.credentialmanager.model.Request 24 import androidx.test.filters.SmallTest 25 import androidx.test.ext.junit.runners.AndroidJUnit4 26 import org.junit.Test 27 import org.mockito.kotlin.mock 28 import org.junit.runner.RunWith 29 import com.android.credentialmanager.model.CredentialType 30 import com.google.common.truth.Truth.assertThat 31 import com.android.credentialmanager.ui.mappers.toGet 32 import com.android.credentialmanager.model.get.ProviderInfo 33 import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry.PerNameEntries 34 35 /** Unit tests for [CredentialSelectorUiStateGetMapper]. */ 36 @SmallTest 37 @RunWith(AndroidJUnit4::class) 38 class CredentialSelectorUiStateGetMapperTest { 39 40 private val mDrawable = mock<Drawable>() 41 42 private val actionEntryInfo = 43 ActionEntryInfo( 44 providerId = "", 45 entryKey = "", 46 entrySubkey = "", 47 pendingIntent = null, 48 fillInIntent = null, 49 title = "title", 50 icon = mDrawable, 51 subTitle = "subtitle", 52 ) 53 54 private val authenticationEntryInfo = 55 AuthenticationEntryInfo( 56 providerId = "", 57 entryKey = "", 58 entrySubkey = "", 59 pendingIntent = null, 60 fillInIntent = null, 61 title = "title", 62 providerDisplayName = "", 63 icon = mDrawable, 64 isUnlockedAndEmpty = true, 65 isLastUnlocked = true 66 ) 67 68 private val passkeyCredentialEntryInfo = 69 createCredentialEntryInfo(credentialType = CredentialType.PASSKEY, userName = "userName") 70 71 private val unknownCredentialEntryInfo = 72 createCredentialEntryInfo(credentialType = CredentialType.UNKNOWN, userName = "userName2") 73 74 private val passwordCredentialEntryInfo = 75 createCredentialEntryInfo(credentialType = CredentialType.PASSWORD, userName = "userName") 76 77 private val recentlyUsedPasskeyCredential = 78 createCredentialEntryInfo(credentialType = 79 CredentialType.PASSKEY, lastUsedTimeMillis = 2L, userName = "userName") 80 81 private val recentlyUsedPasswordCredential = 82 createCredentialEntryInfo(credentialType = 83 CredentialType.PASSWORD, lastUsedTimeMillis = 2L, userName = "userName") 84 85 private val credentialList1 = listOf( 86 passkeyCredentialEntryInfo, 87 passwordCredentialEntryInfo 88 ) 89 90 private val credentialList2 = listOf( 91 passkeyCredentialEntryInfo, 92 passwordCredentialEntryInfo, 93 recentlyUsedPasskeyCredential, 94 unknownCredentialEntryInfo, 95 recentlyUsedPasswordCredential 96 ) 97 98 @Test On primary screen, just one account returns SingleEntrynull99 fun `On primary screen, just one account returns SingleEntry`() { 100 val getCredentialUiState = Request.Get( 101 token = null, 102 resultReceiver = null, 103 providerInfos = listOf(createProviderInfo(credentialList1))).toGet(isPrimary = true) 104 105 assertThat(getCredentialUiState).isEqualTo( 106 CredentialSelectorUiState.Get.SingleEntry(passkeyCredentialEntryInfo) 107 ) // prefer passkey over password for selected credential 108 } 109 110 @Test On primary screen, multiple accounts returns MultipleEntryPrimaryScreennull111 fun `On primary screen, multiple accounts returns MultipleEntryPrimaryScreen`() { 112 val getCredentialUiState = Request.Get( 113 token = null, 114 resultReceiver = null, 115 providerInfos = listOf(createProviderInfo(listOf(passkeyCredentialEntryInfo, 116 unknownCredentialEntryInfo)))).toGet(isPrimary = true) 117 118 assertThat(getCredentialUiState).isEqualTo( 119 CredentialSelectorUiState.Get.MultipleEntryPrimaryScreen( 120 sortedEntries = listOf( 121 passkeyCredentialEntryInfo, // userName 122 unknownCredentialEntryInfo // userName2 123 ), 124 icon = mDrawable, 125 authenticationEntryList = listOf(authenticationEntryInfo) 126 )) // prefer passkey from account 1, then unknown from account 2 127 } 128 129 @Test On secondary screen, a MultipleEntry is returnednull130 fun `On secondary screen, a MultipleEntry is returned`() { 131 val getCredentialUiState = Request.Get( 132 token = null, 133 resultReceiver = null, 134 providerInfos = listOf(createProviderInfo(credentialList1))).toGet(isPrimary = false) 135 136 assertThat(getCredentialUiState).isEqualTo( 137 CredentialSelectorUiState.Get.MultipleEntry( 138 listOf(PerNameEntries("userName", listOf( 139 passkeyCredentialEntryInfo, 140 passwordCredentialEntryInfo)) 141 ), 142 listOf(actionEntryInfo), 143 listOf(authenticationEntryInfo) 144 )) 145 } 146 147 @Test Returned multiple entry is sorted by credentialType and lastUsedTimeMillisnull148 fun `Returned multiple entry is sorted by credentialType and lastUsedTimeMillis`() { 149 val getCredentialUiState = Request.Get( 150 token = null, 151 resultReceiver = null, 152 providerInfos = listOf(createProviderInfo(credentialList1), 153 createProviderInfo(credentialList2))).toGet(isPrimary = false) 154 155 assertThat(getCredentialUiState).isEqualTo( 156 CredentialSelectorUiState.Get.MultipleEntry( 157 listOf( 158 PerNameEntries("userName", 159 listOf( 160 recentlyUsedPasskeyCredential, // from provider 2 161 passkeyCredentialEntryInfo, // from provider 1 or 2 162 passkeyCredentialEntryInfo, // from provider 1 or 2 163 recentlyUsedPasswordCredential, // from provider 2 164 passwordCredentialEntryInfo, // from provider 1 or 2 165 passwordCredentialEntryInfo, // from provider 1 or 2 166 )), 167 PerNameEntries("userName2", listOf(unknownCredentialEntryInfo)), 168 ), 169 listOf(actionEntryInfo, actionEntryInfo), 170 listOf(authenticationEntryInfo, authenticationEntryInfo) 171 ) 172 ) 173 } 174 175 @Test Returned multiple entry is grouped by display name if presentnull176 fun `Returned multiple entry is grouped by display name if present`() { 177 val testCred1 = createCredentialEntryInfo(displayName = "testDisplayName", 178 userName = "testUserName", credentialType = CredentialType.PASSWORD) 179 val testCred2 = createCredentialEntryInfo(displayName = "testDisplayName", 180 userName = "testUserName", credentialType = CredentialType.PASSKEY) 181 val getCredentialUiState = Request.Get( 182 token = null, 183 resultReceiver = null, 184 providerInfos = listOf(createProviderInfo(credentialList1), 185 createProviderInfo(credentialList2), 186 createProviderInfo(listOf(testCred1, testCred2)))) 187 .toGet(isPrimary = false) 188 189 assertThat(getCredentialUiState).isEqualTo( 190 CredentialSelectorUiState.Get.MultipleEntry( 191 listOf( 192 PerNameEntries("userName", 193 listOf( 194 recentlyUsedPasskeyCredential, // from provider 2 195 passkeyCredentialEntryInfo, // from provider 1 or 2 196 passkeyCredentialEntryInfo, // from provider 1 or 2 197 recentlyUsedPasswordCredential, // from provider 2 198 passwordCredentialEntryInfo, // from provider 1 or 2 199 passwordCredentialEntryInfo, // from provider 1 or 2 200 )), 201 PerNameEntries("userName2", listOf(unknownCredentialEntryInfo)), 202 PerNameEntries("testDisplayName", listOf(testCred2, testCred1)), 203 ), 204 listOf(actionEntryInfo, actionEntryInfo, actionEntryInfo), 205 listOf(authenticationEntryInfo, authenticationEntryInfo, authenticationEntryInfo) 206 ) 207 ) 208 } 209 createCredentialEntryInfonull210 fun createCredentialEntryInfo( 211 userName: String, 212 displayName: String? = null, 213 credentialType: CredentialType = CredentialType.PASSKEY, 214 lastUsedTimeMillis: Long = 0L 215 ): CredentialEntryInfo = 216 CredentialEntryInfo( 217 providerId = "", 218 entryKey = "", 219 entrySubkey = "", 220 pendingIntent = null, 221 fillInIntent = null, 222 credentialType = credentialType, 223 rawCredentialType = "", 224 credentialTypeDisplayName = "", 225 providerDisplayName = "", 226 userName = userName, 227 displayName = displayName, 228 icon = mDrawable, 229 shouldTintIcon = false, 230 lastUsedTimeMillis = Instant.ofEpochMilli(lastUsedTimeMillis), 231 isAutoSelectable = true, 232 entryGroupId = "", 233 isDefaultIconPreferredAsSingleProvider = false, 234 affiliatedDomain = "", 235 ) 236 237 fun createProviderInfo(credentials: List<CredentialEntryInfo> = listOf()): ProviderInfo = 238 ProviderInfo( 239 id = "providerInfo", 240 icon = mDrawable, 241 displayName = "displayName", 242 credentialEntryList = credentials, 243 authenticationEntryList = listOf(authenticationEntryInfo), 244 remoteEntry = null, 245 actionEntryList = listOf(actionEntryInfo) 246 ) 247 } 248