test(auth): cover refresh route operation id forwarding
Add AuthRoutesTest to lock in the contract for /v1/auth/refresh:
- forwards a valid refreshOperationId UUID into SessionService.refresh
- returns 400 with invalid_request when the operation id is malformed
These tests back the idempotent session refresh behaviour introduced in
4c9e5fe ('Make session refresh retries idempotent').
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
package com.osglab.account.features.auth
|
||||
|
||||
import com.osglab.account.common.api.installApiStatusPages
|
||||
import com.osglab.account.common.security.SESSION_AUTH_NAME
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.string.shouldContain
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.contentType
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.auth.Authentication
|
||||
import io.ktor.server.auth.bearer
|
||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.server.routing.routing
|
||||
import io.ktor.server.testing.testApplication
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import java.time.Instant
|
||||
import java.util.UUID
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.test.Test
|
||||
|
||||
class AuthRoutesTest {
|
||||
@Test
|
||||
fun `refresh route forwards the stable operation identifier`() = testApplication {
|
||||
val operationId = UUID.fromString("10000000-0000-0000-0000-000000000029")
|
||||
val accountId = UUID.fromString("20000000-0000-0000-0000-000000000029")
|
||||
val service = mockk<SessionService>()
|
||||
coEvery { service.refresh("refresh-from-ios", operationId) } returns
|
||||
SessionTokens(
|
||||
accountId = accountId,
|
||||
accessToken = "access-replacement",
|
||||
accessTokenExpiresAt = Instant.ofEpochSecond(2_000_000_000),
|
||||
refreshToken = "refresh-replacement",
|
||||
refreshTokenExpiresAt = Instant.ofEpochSecond(2_100_000_000),
|
||||
)
|
||||
application {
|
||||
installTestAuthRoutes(service)
|
||||
}
|
||||
|
||||
val response = client.post("/v1/auth/refresh") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(
|
||||
"""
|
||||
{
|
||||
"refreshToken": "refresh-from-ios",
|
||||
"refreshOperationId": "$operationId"
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
response.status shouldBe HttpStatusCode.OK
|
||||
response.bodyAsText() shouldContain """"refreshToken":"refresh-replacement""""
|
||||
coVerify(exactly = 1) { service.refresh("refresh-from-ios", operationId) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refresh route rejects a malformed operation identifier`() = testApplication {
|
||||
val service = mockk<SessionService>(relaxed = true)
|
||||
application {
|
||||
installTestAuthRoutes(service)
|
||||
}
|
||||
|
||||
val response = client.post("/v1/auth/refresh") {
|
||||
contentType(ContentType.Application.Json)
|
||||
setBody(
|
||||
"""
|
||||
{
|
||||
"refreshToken": "refresh-from-ios",
|
||||
"refreshOperationId": "not-a-uuid"
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
}
|
||||
|
||||
response.status shouldBe HttpStatusCode.BadRequest
|
||||
response.bodyAsText() shouldContain """"code":"invalid_request""""
|
||||
coVerify(exactly = 0) { service.refresh(any(), any()) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun io.ktor.server.application.Application.installTestAuthRoutes(
|
||||
service: SessionService,
|
||||
) {
|
||||
install(ContentNegotiation) { json(Json { explicitNulls = false }) }
|
||||
installApiStatusPages()
|
||||
install(Authentication) {
|
||||
bearer(SESSION_AUTH_NAME) {
|
||||
authenticate { null }
|
||||
}
|
||||
}
|
||||
routing { authRoutes(service) }
|
||||
}
|
||||
Reference in New Issue
Block a user