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.PerUserNameEntries
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 SingleEntryPerAccountnull111     fun `On primary screen, multiple accounts returns SingleEntryPerAccount`() {
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(PerUserNameEntries("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                     PerUserNameEntries("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                     PerUserNameEntries("userName2", listOf(unknownCredentialEntryInfo)),
168                 ),
169                 listOf(actionEntryInfo, actionEntryInfo),
170                 listOf(authenticationEntryInfo, authenticationEntryInfo)
171             )
172         )
173     }
174 
createCredentialEntryInfonull175     fun createCredentialEntryInfo(
176         userName: String,
177         credentialType: CredentialType = CredentialType.PASSKEY,
178         lastUsedTimeMillis: Long = 0L
179     ): CredentialEntryInfo =
180         CredentialEntryInfo(
181             providerId = "",
182             entryKey = "",
183             entrySubkey = "",
184             pendingIntent = null,
185             fillInIntent = null,
186             credentialType = credentialType,
187             rawCredentialType = "",
188             credentialTypeDisplayName = "",
189             providerDisplayName = "",
190             userName = userName,
191             displayName = "",
192             icon = mDrawable,
193             shouldTintIcon = false,
194             lastUsedTimeMillis = Instant.ofEpochMilli(lastUsedTimeMillis),
195             isAutoSelectable = true,
196             entryGroupId = "",
197             isDefaultIconPreferredAsSingleProvider = false,
198             affiliatedDomain = "",
199         )
200 
201     fun createProviderInfo(credentials: List<CredentialEntryInfo> = listOf()): ProviderInfo =
202         ProviderInfo(
203             id = "providerInfo",
204             icon = mDrawable,
205             displayName = "displayName",
206             credentialEntryList = credentials,
207             authenticationEntryList = listOf(authenticationEntryInfo),
208             remoteEntry = null,
209             actionEntryList = listOf(actionEntryInfo)
210         )
211 }
212