From 505515f746c7bfdad1056dd4ac3529a86aff430c Mon Sep 17 00:00:00 2001 From: Rocky <72559939+hkgood@users.noreply.github.com> Date: Sun, 30 Aug 2026 12:10:56 +0800 Subject: [PATCH] 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'). --- .../account/features/auth/AuthRoutesTest.kt | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/test/kotlin/com/osglab/account/features/auth/AuthRoutesTest.kt diff --git a/src/test/kotlin/com/osglab/account/features/auth/AuthRoutesTest.kt b/src/test/kotlin/com/osglab/account/features/auth/AuthRoutesTest.kt new file mode 100644 index 0000000..9cb3bbf --- /dev/null +++ b/src/test/kotlin/com/osglab/account/features/auth/AuthRoutesTest.kt @@ -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() + 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(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) } +}