OkHttp Authenticatorでのtokenの更新処理とUnitTest

# 概要

OkHttpのAuthenticatorを利用することでステータスコードが`401 Unauthorized`（認証の失敗）であった場合に任意の処理を実行できます。

# Authenticatorの実装例

次のコードはAuthenticatorの実装例です。リクエストにAuthorization Headerでtokenを付与する必要があるAPIを想定しています。

```kotlin

class MyAuthenticator(private val repository: AuthenticationRepository) : Authenticator {

    private val Response.retryCount: Int
        get() {
            var currentResponse = priorResponse()
            var result = 1
            while (currentResponse != null) {
                result++
                currentResponse = currentResponse.priorResponse()
            }
            return result
        }

    override fun authenticate(route: Route?, response: Response): Request? = when {
        response.retryCount >= 3 -> null
        else -> {
            val token = runBlocking { repository.refreshToken() }
            response.request()
                .newBuilder()
                .addHeader("Authorization", "Bearer ${token}")
                .build()
        }
    }
}
```

`response.retryCount`が3未満の場合にtokenの更新と再リクエストを行います。この実装ではリトライ回数は3回です。

```kotlin
override fun authenticate(route: Route?, response: Response): Request? = when {
    response.retryCount >= 3 -> null
    else -> {
        // tokenの更新と再リクエスト
    }
}
```

`retryCount`はResponseクラスの拡張関数として実装していますが、これは[こちらの記事](https://www.lordcodes.com/articles/authorization-of-web-requests-for-okhttp-and-retrofit)の実装を参考にしています(綺麗な実装で好きです)。

tokenを更新する処理は次のinterfaceで定義していて、`repository.refreshToken()`のようにしてtokenの更新処理を行います。

```kotlin
interface AuthenticationRepository {
    suspend fun refreshToken(): String
}
```

suspend functionなのでAuthenticatorでは`runBlocking`で実行します。

```kotlin
val token = runBlocking { repository.refreshToken() }
```

# 使用方法

OkHttp Clientを作成する際に`authenticator`に実装したAuthenticatorをセットすることで使用できます。

```kotlin
val client: OkHttpClient = OkHttpClient().newBuilder()
    .authenticator(MyAuthenticator(authenticationRepository))
    .build()
```

# AuthenticatorのUnitTest

Authenticatorのテストも[interceptorと同様](https://qiita.com/Horie1024/items/39ba191e1783de9163d1)に書けます。次のテストコードは1度目のリトライで認証が成功した場合を想定します。モックライブラリには[Mockk](https://github.com/mockk/mockk)、アサーションライブラリには[Truth](https://github.com/google/truth)を使用しています。

```kotlin

@RunWith(AndroidJUnit4::class)
class AuthenticatorTest {

    @get:Rule
    val server: MockWebServer = MockWebServer()

    @Test
    fun authenticatorTest() {

        // MockResponseの用意
        server.enqueue(MockResponse().apply {
            status = "HTTP/1.1 401 Unauthorized"
        })
        server.enqueue(MockResponse().apply {
            status = "HTTP/1.1 200 OK"
        })

        // refreshTokenが成功し新しいtokenが返るよう指定
        val repository = mockk<AuthenticationRepository>()
        coEvery { repository.refreshToken() } returns "NEW_TOKEN"

        val client: OkHttpClient = OkHttpClient().newBuilder()
            .authenticator(MyAuthenticator(repository))
            .build()
        val response = client.newCall(Request.Builder().url(server.url("/")).build()).execute()
        
        // refreshToken関数が実行されていることを検証
        coVerify { repository.refreshToken() }

        // 想定通りHeaderにtokenが指定されていることを検証
        Truth.assertThat(response.request().header("Authorization")).isEqualTo("Bearer NEW_TOKEN")
    }
}
```

このテストコードでは、ステータスコード`HTTP/1.1 401 Unauthorized`と、`HTTP/1.1 200 OK`をセットした2つのMockResponseのそれぞれMockWebServerにenqueueします。そして、`AuthenticationRepository`をモックし、新しいtokenを返すよう指定します。その後テスト対象のAuthenticatorをセットしたclientを作成しリクエストを実行、最後にrequestを検証します。

また、3回以上リトライを行った場合のテストは次のように書けます。

```kotlin

@RunWith(AndroidJUnit4::class)
class AuthenticatorTest {

    @get:Rule
    val server: MockWebServer = MockWebServer()

    @Test
    fun authenticatorTest() {

        repeat(4) {
            server.enqueue(MockResponse().apply {
                status = "HTTP/1.1 401 Unauthorized"
            })
        }

        val repository = mockk<AuthenticationRepository>()
        coEvery { repository.refreshToken() } returns ""

        val client: OkHttpClient = OkHttpClient().newBuilder()
            .authenticator(MyAuthenticator(repository))
            .build()
        val response = client.newCall(Request.Builder().url(server.url("/")).build()).execute()

        coVerify { repository.refreshToken() }

        Truth.assertThat(responseCount(response)).isEqualTo(3)
        Truth.assertThat(response.isSuccessful).isFalse()
    }

    private fun responseCount(response: Response): Int {
        var currentResponse = response.priorResponse()
        var result = 1
        while (currentResponse != null) {
            result++
            currentResponse = currentResponse.priorResponse()
        }
        return result
    }
}
```

## 参考

- https://square.github.io/okhttp/recipes/#handling-authentication-kt-java
- https://github.com/yschimke/okhttp/blob/f9171936cac4cfc607b622951e7e65a085781a6a/okhttp/src/main/kotlin/okhttp3/Authenticator.kt
    - コメントにリトライ回数をカウントするサンプルコードが書かれています。
- https://www.lordcodes.com/articles/authorization-of-web-requests-for-okhttp-and-retrofit