LoginSignup
23
15

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-09-17

概要

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

Authenticatorの実装例

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


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回です。

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

retryCountはResponseクラスの拡張関数として実装していますが、これはこちらの記事の実装を参考にしています(綺麗な実装で好きです)。

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

interface AuthenticationRepository {
    suspend fun refreshToken(): String
}

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

val token = runBlocking { repository.refreshToken() }

使用方法

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

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

AuthenticatorのUnitTest

Authenticatorのテストもinterceptorと同様に書けます。次のテストコードは1度目のリトライで認証が成功した場合を想定します。モックライブラリにはMockk、アサーションライブラリにはTruthを使用しています。


@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回以上リトライを行った場合のテストは次のように書けます。


@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
    }
}

参考

23
15
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
15