認証を モック化したい ことがある
KtorアプリケーションのRoute上のAPIをテストしたい場合、ログイン等の認証が必須のものについては、何らかの方法で認証を突破する必要があります。
認証方式によって突破方法はさまざまですが、今回はKtorのAuthenticationプラグイン(Feature)を拡張実装するアプローチをご紹介します。
代表的な恩恵は、HTTPリクエストヘッダーのAuthorization属性にモックの情報の差し込む、などの小細工が必要なくなることでしょうか。さっそく見ていきましょう。
1. モック認証用の拡張関数の作成
KtorのAuthentication
プラグインにデフォルトで実装されている*formやbasic*といった関数を参考に、セットした認証主体の情報を返すだけの拡張関数を作成します。
実装すべきは大きく2つ、①Authentication.Configuration
の拡張関数と、②拡張関数中で利用するAuthenticationProvider
の継承クラスです。
// io.ktor.auth.Authenticationを拡張
fun Authentication.Configuration.mock(
name: String? = null,
configure: MockAuthenticationProvider.Configuration.() -> Unit
) {
val provider = MockAuthenticationProvider.Configuration(name).apply(configure).build()
// 認証時に実行される処理の定義
provider.pipeline.intercept(AuthenticationPipeline.RequestAuthentication) { context ->
// 認証主体の取得関数を実行(関数は別のところで設定)
val principal = provider.principalProvider(call)
// 認証主体がnullなら401を返して認証を中断
if (principal == null) {
call.respond(UnauthorizedResponse())
context.challenge.complete()
return@intercept
}
// (認証情報がnullでない場合)
// 認証主体をcontextにセット -> call.principalで取得可能に
context.principal(principal)
}
register(provider)
}
// io.ktor.auth.AuthenticationProviderを継承
class MockAuthenticationProvider(config: Configuration) : AuthenticationProvider(config) {
val principalProvider = config.principalProvider
// io.ktor.auth.AuthenticationProvider.Configurationを継承
class Configuration(name: String?) : AuthenticationProvider.Configuration(name) {
var principalProvider: ApplicationCall.() -> Principal? = { null }
fun build() = MockAuthenticationProvider(this)
// 「認証主体の情報をセットする関数」を渡す関数
fun principal(principalProvider: ApplicationCall.() -> Principal?) {
this.principalProvider = principalProvider
}
}
}
2. モック認証のinstallerの実装
認証プラグインをinstallする拡張関数を実装します。
ここでprincipal{ }
関数に渡した情報が、ルーティング上の*call.principal()*から取得されます。
利用ごとに認証情報を切り替えたい場合は、関数の引数などから情報を差し込めるようにしておきます。以下、サンプルです。
// io.ktor.application.Application
fun Application.testAuth(loginUser: LoginUser? = null) {
install(Authentication) {
// 1.で作成した関数を呼び出し
mock {
// ログイン主体(Principal)の取得ロジックを記載
principal { loginUser }
}
}
}
3. アプリケーションへの組み込み
モック認証を利用したいアプリケーション(のモジュール)に、モック認証のinstallerを追加します。
本番運用でモック認証は使わないでしょうから、
- テスト時と本番利用時の実行モジュールを分けて定義
- それぞれ別の認証installerを追加
とした上、テスト時には認証をモック化したモジュールを利用するようにします。
// テスト時に実行するモジュール
fun Application.testModule(loginUser: LoginUser? = null) {
testAuth(loginUser)
others()
}
// 本番利用時に実行するモジュール
fun Application.mainModule() {
productionAuth() //mockでないauth
others()
}
// 認証以外のモジュール
fun Application.others() {
router()
// ...and other modules
}
fun Application.router() {
routing {
authenticate {
get("/hello") {
val loginUser = call.principal<LoginUser>()!!
// ...and some logics
}
}
}
}
4. 使う
あとは「テスト用のモジュール」の利用場面で、認証主体を指定するだけ。
以下は前掲のモジュールを利用したテストのサンプルです。testModuleの引数に渡した「ログインユーザー(loginUser)」が、そのままルーティング上の*call.principal()*から取り出されます。
// Ktorのテストライブラリを利用してApiのテストをするサンプル
class Test {
@Test
fun `authenticated hello - success`() {
// io.ktor.server.testing.withTestApplication
withTestApplication({ testModule(loginUser = TestLoginUser.someone) }) {
handleRequest(HttpMethod.Get, "/hello") { }
.apply {
// ...some assertions
}
}
}
}
むすび
モック認証の都合が利用者側に露出しにくい(魔法のパラメータなどを仕込まなくていい)のは気に入っています。
認証をテスト時にモック化したいというニーズはそれなりに普遍的なはずが、情報が少ないのが意外…。ご参考までに。ポン・チャガール1