Skip to content

Commit 62d5ca5

Browse files
committed
Added test cases for MyAccountApi
1 parent d6d80d1 commit 62d5ca5

7 files changed

Lines changed: 493 additions & 3 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.auth0.android.callback
2+
3+
import com.auth0.android.myaccount.MyAccountException
4+
5+
public interface MyAccountCallback<T> : Callback<T, MyAccountException>

auth0/src/main/java/com/auth0/android/myaccount/MyAccountAPIClient.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -163,14 +163,14 @@ public class MyAccountAPIClient @VisibleForTesting(otherwise = VisibleForTesting
163163
else -> emptyList()
164164
}
165165
}
166+
val locationHeader = headers[LOCATION_KEY]?.get(0)?.split("/")?.lastOrNull()
167+
locationHeader ?: throw MyAccountException("Authentication ID not found")
166168
val authenticationId =
167169
URLDecoder.decode(
168-
headers[LOCATION_KEY]?.get(0)?.split("/")?.lastOrNull(),
170+
locationHeader,
169171
"UTF-8"
170172
)
171173

172-
authenticationId ?: throw MyAccountException("Authentication ID not found")
173-
174174
val passkeyRegistrationChallenge = gson.fromJson<PasskeyRegistrationChallenge>(
175175
reader, PasskeyRegistrationChallenge::class.java
176176
)
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
package com.auth0.android.myaccount
2+
3+
import com.auth0.android.Auth0
4+
import com.auth0.android.request.PublicKeyCredentials
5+
import com.auth0.android.request.Response
6+
import com.auth0.android.result.PasskeyAuthenticationMethod
7+
import com.auth0.android.result.PasskeyEnrollmentChallenge
8+
import com.auth0.android.util.AuthenticationAPIMockServer.Companion.SESSION_ID
9+
import com.auth0.android.util.MockMyAccountCallback
10+
import com.auth0.android.util.MyAccountAPIMockServer
11+
import com.auth0.android.util.SSLTestUtils.testClient
12+
import com.google.gson.Gson
13+
import com.google.gson.GsonBuilder
14+
import com.google.gson.reflect.TypeToken
15+
import com.nhaarman.mockitokotlin2.mock
16+
import okhttp3.mockwebserver.RecordedRequest
17+
import org.hamcrest.MatcherAssert.assertThat
18+
import org.hamcrest.Matchers
19+
import org.junit.After
20+
import org.junit.Before
21+
import org.junit.Test
22+
import org.junit.runner.RunWith
23+
import org.mockito.MockitoAnnotations
24+
import org.robolectric.RobolectricTestRunner
25+
import org.robolectric.annotation.Config
26+
import java.util.Map
27+
28+
29+
@RunWith(RobolectricTestRunner::class)
30+
@Config(manifest = Config.NONE)
31+
public class MyAccountAPIClientTest {
32+
33+
private lateinit var client: MyAccountAPIClient
34+
private lateinit var gson: Gson
35+
private lateinit var mockAPI: MyAccountAPIMockServer
36+
37+
@Before
38+
public fun setUp() {
39+
mockAPI = MyAccountAPIMockServer()
40+
MockitoAnnotations.openMocks(this)
41+
gson = GsonBuilder().serializeNulls().create()
42+
client = MyAccountAPIClient(auth0, ACCESS_TOKEN)
43+
}
44+
45+
@After
46+
public fun tearDown() {
47+
mockAPI.shutdown()
48+
}
49+
50+
@Test
51+
public fun `passkeyEnrollmentChallenge should build correct URL`() {
52+
val callback = MockMyAccountCallback<PasskeyEnrollmentChallenge>()
53+
client.passkeyEnrollmentChallenge()
54+
.start(callback)
55+
val request = mockAPI.takeRequest()
56+
assertThat(request.path, Matchers.equalTo("/me/v1/authentication-methods"))
57+
}
58+
59+
@Test
60+
public fun `passkeyEnrollmentChallenge should include correct parameters`() {
61+
val callback = MockMyAccountCallback<PasskeyEnrollmentChallenge>()
62+
client.passkeyEnrollmentChallenge(userIdentity = USER_IDENTITY, connection = CONNECTION)
63+
.start(callback)
64+
val request = mockAPI.takeRequest()
65+
val body = bodyFromRequest<String>(request)
66+
assertThat(body, Matchers.hasEntry("type", "passkey"))
67+
assertThat(body, Matchers.hasEntry("identity_user_id", USER_IDENTITY))
68+
assertThat(body, Matchers.hasEntry("connection", CONNECTION))
69+
}
70+
71+
72+
@Test
73+
public fun `passkeyEnrollmentChallenge should include Authorization header`() {
74+
val callback = MockMyAccountCallback<PasskeyEnrollmentChallenge>()
75+
client.passkeyEnrollmentChallenge()
76+
.start(callback)
77+
78+
val request = mockAPI.takeRequest()
79+
val header = request.getHeader("Authorization")
80+
81+
assertThat(
82+
header, Matchers.`is`(
83+
"Bearer $ACCESS_TOKEN"
84+
)
85+
)
86+
}
87+
88+
@Test
89+
public fun `passkeyEnrollmentChallenge should throw exception if Location header is missing`() {
90+
mockAPI.willReturnPasskeyChallengeWithoutHeader()
91+
var error: MyAccountException? = null
92+
try {
93+
client.passkeyEnrollmentChallenge()
94+
.execute()
95+
} catch (ex: MyAccountException) {
96+
error = ex
97+
}
98+
mockAPI.takeRequest()
99+
assertThat(error, Matchers.notNullValue())
100+
assertThat(error?.message, Matchers.`is`("Authentication ID not found"))
101+
}
102+
103+
104+
@Test
105+
public fun `passkeyEnrollmentChallenge should parse successful response with encoded authentication ID`() {
106+
mockAPI.willReturnPasskeyChallenge()
107+
val response = client.passkeyEnrollmentChallenge()
108+
.execute()
109+
mockAPI.takeRequest()
110+
assertThat(response, Matchers.`is`(Matchers.notNullValue()))
111+
assertThat(response.authSession, Matchers.comparesEqualTo(SESSION_ID))
112+
assertThat(response.authenticationMethodId, Matchers.comparesEqualTo("passkey|new"))
113+
assertThat(response.authParamsPublicKey.relyingParty.id, Matchers.comparesEqualTo("rpId"))
114+
assertThat(
115+
response.authParamsPublicKey.relyingParty.name,
116+
Matchers.comparesEqualTo("rpName")
117+
)
118+
}
119+
120+
@Test
121+
public fun `enroll should build correct URL`() {
122+
val callback = MockMyAccountCallback<PasskeyAuthenticationMethod>()
123+
val enrollmentChallenge = PasskeyEnrollmentChallenge(
124+
authenticationMethodId = AUTHENTICATION_ID,
125+
authSession = AUTH_SESSION,
126+
authParamsPublicKey = mock()
127+
)
128+
129+
client.enroll(mockPublicKeyCredentials, enrollmentChallenge)
130+
.start(callback)
131+
val request = mockAPI.takeRequest()
132+
assertThat(
133+
request.path,
134+
Matchers.equalTo("/me/v1/authentication-methods/${AUTHENTICATION_ID}/verify")
135+
)
136+
}
137+
138+
@Test
139+
public fun `enroll should include correct parameters and authn_response`() {
140+
val callback = MockMyAccountCallback<PasskeyAuthenticationMethod>()
141+
val enrollmentChallenge = PasskeyEnrollmentChallenge(
142+
authenticationMethodId = AUTHENTICATION_ID,
143+
authSession = AUTH_SESSION,
144+
authParamsPublicKey = mock()
145+
)
146+
client.enroll(mockPublicKeyCredentials, enrollmentChallenge)
147+
.start(callback)
148+
val request = mockAPI.takeRequest()
149+
val body = bodyFromRequest<Any>(request)
150+
assertThat(body, Matchers.hasEntry("auth_session", AUTH_SESSION))
151+
val authnResponse = body["authn_response"] as Map<*, *>
152+
assertThat(authnResponse["authenticatorAttachment"], Matchers.`is`("platform"))
153+
assertThat(authnResponse["id"], Matchers.`is`("id"))
154+
assertThat(authnResponse["rawId"], Matchers.`is`("rawId"))
155+
assertThat(authnResponse["type"], Matchers.`is`("public-key"))
156+
157+
val responseData = authnResponse["response"] as Map<*, *>
158+
assertThat(responseData.containsKey("clientDataJSON"), Matchers.`is`(true))
159+
assertThat(responseData.containsKey("attestationObject"), Matchers.`is`(true))
160+
}
161+
162+
@Test
163+
public fun `enroll should include Authorization header`() {
164+
165+
val callback = MockMyAccountCallback<PasskeyAuthenticationMethod>()
166+
val enrollmentChallenge = PasskeyEnrollmentChallenge(
167+
authenticationMethodId = AUTHENTICATION_ID,
168+
authSession = AUTH_SESSION,
169+
authParamsPublicKey = mock()
170+
)
171+
client.enroll(mockPublicKeyCredentials, enrollmentChallenge)
172+
.start(callback)
173+
174+
val request = mockAPI.takeRequest()
175+
val header = request.getHeader("Authorization")
176+
177+
assertThat(
178+
header, Matchers.`is`(
179+
"Bearer $ACCESS_TOKEN"
180+
)
181+
)
182+
}
183+
184+
185+
@Test
186+
public fun `enroll should return PasskeyAuthenticationMethod on success`() {
187+
mockAPI.willReturnPasskeyAuthenticationMethod()
188+
val enrollmentChallenge = PasskeyEnrollmentChallenge(
189+
authenticationMethodId = AUTHENTICATION_ID,
190+
authSession = AUTH_SESSION,
191+
authParamsPublicKey = mock()
192+
)
193+
val response = client.enroll(mockPublicKeyCredentials, enrollmentChallenge)
194+
.execute()
195+
mockAPI.takeRequest()
196+
assertThat(response, Matchers.`is`(Matchers.notNullValue()))
197+
assertThat(response.id, Matchers.comparesEqualTo("auth_method_123456789"))
198+
assertThat(response.type, Matchers.comparesEqualTo("passkey"))
199+
assertThat(response.credentialDeviceType, Matchers.comparesEqualTo("phone"))
200+
assertThat(response.credentialBackedUp, Matchers.comparesEqualTo(true))
201+
assertThat(response.publicKey, Matchers.comparesEqualTo("publickey"))
202+
}
203+
204+
205+
private fun <T> bodyFromRequest(request: RecordedRequest): kotlin.collections.Map<String, T> {
206+
val mapType = object : TypeToken<kotlin.collections.Map<String?, T>?>() {}.type
207+
return gson.fromJson(request.body.readUtf8(), mapType)
208+
}
209+
210+
private val auth0: Auth0
211+
get() {
212+
val auth0 = Auth0.getInstance(CLIENT_ID, mockAPI.domain, mockAPI.domain)
213+
auth0.networkingClient = testClient
214+
return auth0
215+
}
216+
217+
private val mockPublicKeyCredentials = PublicKeyCredentials(
218+
id = "id",
219+
rawId = "rawId",
220+
type = "public-key",
221+
clientExtensionResults = mock(),
222+
response = Response(
223+
authenticatorData = "authenticatordaya",
224+
clientDataJSON = "eyJ0eXBlIjoiaHR0cHM6Ly9jcmVkZW50aWFscy5leGFtcGxlLm9yZy93ZWJhdXRobi9jcmVhdGUiLCJjaGFsbGVuZ2UiOiJZMmhoYkd4bGJtZGxVbUZ1Wkc5dFFubDBaWE5GYm1OdlpHVmtTVzVDWVhObE5qUT0iLCJvcmlnaW4iOiJleGFtcGxlLmF1dGgwLmNvbSIsImNyb3NzT3JpZ2luIjpmYWxzZX0",
225+
attestationObject = "o2NmbXRmcGFja2VkZ2F0dFN0bXSiY2FsZyZjc2lnWEgwRgIhAI4b7RFy4MnMqD4jDtl8BCpE5vvDmQSMHjZ7xZHlKFYiAiEA0GC_QoOve_71eMHlWAzM-YzQdGfNEZVVx3m_cNCJXAZoYXV0aERhdGFYJKlzaWduYXR1cmVEYXRhX19fX19fX19fX19fX19fX19fX19fUKI",
226+
transports = listOf("str"),
227+
signature = "signature",
228+
userHandle = "user"
229+
),
230+
authenticatorAttachment = "platform"
231+
)
232+
233+
private companion object {
234+
private const val CLIENT_ID = "CLIENTID"
235+
private const val USER_IDENTITY = "user123"
236+
private const val CONNECTION = "passkey-connection"
237+
private const val ACCESS_TOKEN = "accessToken"
238+
private const val AUTHENTICATION_ID = "authId123"
239+
private const val AUTH_SESSION = "session456"
240+
}
241+
}
242+
243+

auth0/src/test/java/com/auth0/android/util/APIMockServer.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ internal abstract class APIMockServer {
2727
.setBody(json)
2828
}
2929

30+
fun responseWithJSON(json: String, statusCode: Int, header: Map<String, String>): MockResponse {
31+
val response = MockResponse()
32+
.setResponseCode(statusCode)
33+
.addHeader("Content-Type", "application/json")
34+
.setBody(json)
35+
36+
header.forEach { (key, value) ->
37+
response.addHeader(key, value)
38+
}
39+
return response
40+
}
41+
3042
init {
3143
server.start()
3244
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.auth0.android.util
2+
3+
import com.auth0.android.callback.MyAccountCallback
4+
import com.auth0.android.myaccount.MyAccountException
5+
import java.util.concurrent.Callable
6+
7+
public class MockMyAccountCallback<T> : MyAccountCallback<T> {
8+
9+
private var error: MyAccountException? = null
10+
private var payload: T? = null
11+
12+
override fun onSuccess(result: T) {
13+
this.payload = result
14+
}
15+
16+
override fun onFailure(error: MyAccountException) {
17+
this.error = error
18+
}
19+
20+
public fun error(): Callable<MyAccountException?> = Callable { error }
21+
22+
public fun payload(): Callable<T?> = Callable { payload }
23+
24+
public fun getError(): MyAccountException? = error
25+
26+
public fun getPayload(): T? = payload
27+
}

0 commit comments

Comments
 (0)