Establish secure account and managed AI backend
Provide the production foundation for Apple identity, immutable credits, referrals, integrity checks, managed providers, and hardened Docker deployment.
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
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",
|
||||
)
|
||||
Reference in New Issue
Block a user