1+ package com.auth0.android.request
2+
3+ import com.auth0.android.dpop.DPoPKeyStore
4+ import com.auth0.android.dpop.DPoPProvider
5+ import com.auth0.android.dpop.FakeECPrivateKey
6+ import com.auth0.android.dpop.FakeECPublicKey
7+ import com.nhaarman.mockitokotlin2.any
8+ import com.nhaarman.mockitokotlin2.argumentCaptor
9+ import com.nhaarman.mockitokotlin2.mock
10+ import com.nhaarman.mockitokotlin2.times
11+ import com.nhaarman.mockitokotlin2.verify
12+ import com.nhaarman.mockitokotlin2.whenever
13+ import okhttp3.Interceptor
14+ import okhttp3.Protocol
15+ import okhttp3.Request
16+ import okhttp3.RequestBody.Companion.toRequestBody
17+ import okhttp3.Response
18+ import okhttp3.ResponseBody.Companion.toResponseBody
19+ import org.hamcrest.CoreMatchers.`is`
20+ import org.hamcrest.CoreMatchers.not
21+ import org.hamcrest.CoreMatchers.nullValue
22+ import org.hamcrest.MatcherAssert.assertThat
23+ import org.junit.Before
24+ import org.junit.Test
25+ import org.junit.runner.RunWith
26+ import org.robolectric.RobolectricTestRunner
27+
28+ @RunWith(RobolectricTestRunner ::class )
29+ public class RetryInterceptorTest {
30+
31+ private lateinit var mockChain: Interceptor .Chain
32+ private lateinit var mockKeyStore: DPoPKeyStore
33+
34+ private lateinit var retryInterceptor: RetryInterceptor
35+
36+ @Before
37+ public fun setUp () {
38+ mockChain = mock()
39+ mockKeyStore = mock()
40+
41+ DPoPProvider .keyStore = mockKeyStore
42+ retryInterceptor = RetryInterceptor ()
43+ }
44+
45+ @Test
46+ public fun `should proceed without retry if response is not a DPoP nonce error` () {
47+ val request = createRequest()
48+ val okResponse = createOkResponse(request)
49+ whenever(mockChain.request()).thenReturn(request)
50+ whenever(mockChain.proceed(request)).thenReturn(okResponse)
51+
52+ val result = retryInterceptor.intercept(mockChain)
53+
54+ assertThat(result, `is `(okResponse))
55+ verify(mockChain).proceed(request)
56+ }
57+
58+ @Test
59+ public fun `should retry request when DPoP nonce error occurs and key pair is available` () {
60+ val initialRequest = createRequest(accessToken = " test-access-token" )
61+ val errorResponse = createDpopNonceErrorResponse(initialRequest)
62+ val successResponse = createOkResponse(initialRequest)
63+ val newRequestCaptor = argumentCaptor<Request >()
64+
65+ whenever(mockChain.request()).thenReturn(initialRequest)
66+
67+ whenever(mockChain.proceed(any()))
68+ .thenReturn(errorResponse)
69+ .thenReturn(successResponse)
70+
71+ val mockKeyPair = Pair (FakeECPrivateKey (), FakeECPublicKey ())
72+ whenever(mockKeyStore.hasKeyPair()).thenReturn(true )
73+ whenever(mockKeyStore.getKeyPair()).thenReturn(mockKeyPair)
74+
75+ val result = retryInterceptor.intercept(mockChain)
76+
77+ assertThat(result, `is `(successResponse))
78+ verify(mockChain, times(2 )).proceed(newRequestCaptor.capture())
79+
80+ val retriedRequest = newRequestCaptor.secondValue
81+ assertThat(retriedRequest.header(" DPoP" ), not (nullValue()))
82+ assertThat(retriedRequest.header(" X-Internal-Retry-Count" ), `is `(" 1" ))
83+ assertThat(DPoPProvider .auth0Nonce, `is `(" new-nonce-from-header" ))
84+ }
85+
86+ @Test
87+ public fun `should not retry request when DPoP nonce error occurs and retry count reaches max` () {
88+ val request = createRequest(retryCount = 1 )
89+ val errorResponse = createDpopNonceErrorResponse(request)
90+ whenever(mockChain.request()).thenReturn(request)
91+ whenever(mockChain.proceed(request)).thenReturn(errorResponse)
92+
93+ val result = retryInterceptor.intercept(mockChain)
94+
95+ assertThat(result, `is `(errorResponse))
96+ verify(mockChain).proceed(request)
97+ }
98+
99+ @Test
100+ public fun `should not retry request when DPoP nonce error occurs but proof generation fails` () {
101+ val request = createRequest()
102+ val errorResponse = createDpopNonceErrorResponse(request)
103+ whenever(mockChain.request()).thenReturn(request)
104+ whenever(mockChain.proceed(request)).thenReturn(errorResponse)
105+
106+ whenever(mockKeyStore.hasKeyPair()).thenReturn(false )
107+
108+ val result = retryInterceptor.intercept(mockChain)
109+
110+ assertThat(result, `is `(errorResponse))
111+ verify(mockChain).proceed(request)
112+ }
113+
114+ @Test
115+ public fun `should handle initial request with no retry header` () {
116+ val initialRequest = createRequest(accessToken = " test-access-token" , retryCount = null )
117+ val errorResponse = createDpopNonceErrorResponse(initialRequest)
118+ val successResponse = createOkResponse(initialRequest)
119+ val newRequestCaptor = argumentCaptor<Request >()
120+
121+ whenever(mockChain.request()).thenReturn(initialRequest)
122+ whenever(mockChain.proceed(any()))
123+ .thenReturn(errorResponse)
124+ .thenReturn(successResponse)
125+
126+ val mockKeyPair = Pair (FakeECPrivateKey (), FakeECPublicKey ())
127+ whenever(mockKeyStore.hasKeyPair()).thenReturn(true )
128+ whenever(mockKeyStore.getKeyPair()).thenReturn(mockKeyPair)
129+
130+ val result = retryInterceptor.intercept(mockChain)
131+
132+ assertThat(result, `is `(successResponse))
133+ verify(mockChain, times(2 )).proceed(newRequestCaptor.capture())
134+ val retriedRequest = newRequestCaptor.secondValue
135+ assertThat(retriedRequest.header(" X-Internal-Retry-Count" ), `is `(" 1" ))
136+ }
137+
138+ private fun createRequest (accessToken : String? = null, retryCount : Int? = 0): Request {
139+ val builder = Request .Builder ()
140+ .url(" https://test.com/api" )
141+ .method(" POST" , " {}" .toRequestBody())
142+
143+ if (accessToken != null ) {
144+ builder.header(" Authorization" , " DPoP $accessToken " )
145+ }
146+ if (retryCount != null ) {
147+ builder.header(" X-Internal-Retry-Count" , retryCount.toString())
148+ }
149+ return builder.build()
150+ }
151+
152+ private fun createOkResponse (request : Request ): Response {
153+ return Response .Builder ()
154+ .request(request)
155+ .protocol(Protocol .HTTP_2 )
156+ .code(200 )
157+ .message(" OK" )
158+ .body(" {}" .toResponseBody())
159+ .build()
160+ }
161+
162+ private fun createDpopNonceErrorResponse (request : Request ): Response {
163+ return Response .Builder ()
164+ .request(request)
165+ .protocol(Protocol .HTTP_2 )
166+ .code(401 )
167+ .message(" Unauthorized" )
168+ .header(" WWW-Authenticate" , " DPoP error=\" use_dpop_nonce\" " )
169+ .header(" dpop-nonce" , " new-nonce-from-header" )
170+ .body(" " .toResponseBody())
171+ .build()
172+ }
173+ }
0 commit comments