0af35d44f4
Provide the production foundation for Apple identity, immutable credits, referrals, integrity checks, managed providers, and hardened Docker deployment.
124 lines
4.5 KiB
Kotlin
124 lines
4.5 KiB
Kotlin
package com.osglab.account.features.auth
|
|
|
|
import com.osglab.account.config.AppleConfig
|
|
import io.kotest.assertions.throwables.shouldThrow
|
|
import io.kotest.core.spec.style.FunSpec
|
|
import io.kotest.matchers.shouldBe
|
|
import io.ktor.client.HttpClient
|
|
import io.ktor.client.engine.mock.MockEngine
|
|
import io.ktor.client.engine.mock.respond
|
|
import io.ktor.client.plugins.HttpTimeout
|
|
import io.ktor.client.request.forms.FormDataContent
|
|
import io.ktor.http.HttpHeaders
|
|
import io.ktor.http.HttpStatusCode
|
|
import io.ktor.http.headersOf
|
|
import java.net.SocketTimeoutException
|
|
|
|
class AppleTokenClientTest : FunSpec({
|
|
test("authorization code exchange uses the replaceable HTTP boundary") {
|
|
val engine = MockEngine { request ->
|
|
request.url.toString() shouldBe "https://appleid.apple.com/auth/token"
|
|
val form = (request.body as FormDataContent).formData
|
|
form["client_id"] shouldBe "com.example.ios"
|
|
form["client_secret"] shouldBe "signed-client-secret"
|
|
form["code"] shouldBe "one-time-code"
|
|
form["grant_type"] shouldBe "authorization_code"
|
|
respond(
|
|
content = """{"refresh_token":"refresh","id_token":"identity"}""",
|
|
status = HttpStatusCode.OK,
|
|
headers = headersOf(HttpHeaders.ContentType, "application/json"),
|
|
)
|
|
}
|
|
val client = HttpClient(engine) { install(HttpTimeout) }
|
|
val apple = HttpAppleTokenClient(
|
|
client,
|
|
appleTokenClientConfig(),
|
|
AppleClientSecretSigner { "signed-client-secret" },
|
|
)
|
|
|
|
apple.exchangeAuthorizationCode("one-time-code") shouldBe
|
|
AppleTokenExchange("refresh", "identity")
|
|
|
|
client.close()
|
|
}
|
|
|
|
test("provider timeout is mapped to a retryable failure") {
|
|
val engine = MockEngine {
|
|
throw SocketTimeoutException("simulated provider timeout")
|
|
}
|
|
val client = HttpClient(engine) { install(HttpTimeout) }
|
|
val apple = HttpAppleTokenClient(
|
|
httpClient = client,
|
|
config = appleTokenClientConfig(),
|
|
clientSecretProvider = AppleClientSecretSigner { "signed-client-secret" },
|
|
requestTimeoutMillis = 10,
|
|
)
|
|
|
|
shouldThrow<AppleTokenEndpointException> {
|
|
apple.exchangeAuthorizationCode("one-time-code")
|
|
}.retryable shouldBe true
|
|
|
|
client.close()
|
|
}
|
|
|
|
test("authorization code exchange rejects a response without a refresh token") {
|
|
val engine = MockEngine {
|
|
respond(
|
|
content = """{"id_token":"identity"}""",
|
|
status = HttpStatusCode.OK,
|
|
headers = headersOf(HttpHeaders.ContentType, "application/json"),
|
|
)
|
|
}
|
|
val client = HttpClient(engine) { install(HttpTimeout) }
|
|
val apple = HttpAppleTokenClient(
|
|
client,
|
|
appleTokenClientConfig(),
|
|
AppleClientSecretSigner { "signed-client-secret" },
|
|
)
|
|
|
|
shouldThrow<AppleTokenEndpointException> {
|
|
apple.exchangeAuthorizationCode("one-time-code")
|
|
}.retryable shouldBe false
|
|
|
|
client.close()
|
|
}
|
|
|
|
test("rate limiting is retryable without reflecting the provider response") {
|
|
val engine = MockEngine {
|
|
respond(
|
|
content = "upstream detail that must not be reflected",
|
|
status = HttpStatusCode.TooManyRequests,
|
|
)
|
|
}
|
|
val client = HttpClient(engine) { install(HttpTimeout) }
|
|
val apple = HttpAppleTokenClient(
|
|
client,
|
|
appleTokenClientConfig(),
|
|
AppleClientSecretSigner { "signed-client-secret" },
|
|
)
|
|
|
|
val failure = shouldThrow<AppleTokenEndpointException> {
|
|
apple.exchangeAuthorizationCode("one-time-code")
|
|
}
|
|
failure.retryable shouldBe true
|
|
failure.message shouldBe "Apple rejected the authorization code"
|
|
|
|
client.close()
|
|
}
|
|
|
|
test("token exchange values are redacted from string rendering") {
|
|
AppleTokenExchange("refresh-secret", "identity-secret").toString() shouldBe
|
|
"AppleTokenExchange(refreshToken=[REDACTED], identityToken=[REDACTED])"
|
|
}
|
|
})
|
|
|
|
private fun appleTokenClientConfig() = AppleConfig(
|
|
teamId = "TEAM",
|
|
keyId = "KEY",
|
|
clientId = "com.example.ios",
|
|
privateKeyPem = "unused-by-test-signer",
|
|
jwksUrl = "https://appleid.apple.com/auth/keys",
|
|
tokenUrl = "https://appleid.apple.com/auth/token",
|
|
revokeUrl = "https://appleid.apple.com/auth/revoke",
|
|
)
|