Files
OSGAccountServer/src/test/kotlin/com/osglab/account/config/AppConfigTest.kt
T

236 lines
9.3 KiB
Kotlin

package com.osglab.account.config
import io.ktor.server.config.MapApplicationConfig
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
import java.util.Base64
class AppConfigTest : FunSpec({
test("test configuration can be injected without Apple client credentials") {
val config = AppConfig.from(validConfig("test"))
config.environment shouldBe Environment.TEST
config.apple.clientCredentialsAvailable shouldBe false
config.encryption.key.size shouldBe 32
config.credits.signupTrial shouldBe 1_000
config.credits.referralInviter shouldBe 1_000
config.credits.referralInvitee shouldBe 1_000
}
test("production rejects placeholder secrets") {
val config = validProductionConfig().apply {
put("app.session.secret", "replace-with-secret")
}
shouldThrow<ConfigValidationException> {
AppConfig.from(config)
}
}
test("production accepts complete independent configuration") {
val config = AppConfig.from(validProductionConfig())
config.environment shouldBe Environment.PRODUCTION
config.database.username shouldBe "test"
config.database.migrationUsername shouldBe "test_migrator"
}
test("production accepts enabled admin bootstrap with Argon2 PHC hash") {
val config = validProductionConfig().apply {
put("app.admin.enabled", "true")
put("app.admin.bootstrapEnabled", "true")
put("app.admin.bootstrapOperatorId", "2c031def-4517-4fde-b592-5db3a3eefdf6")
put("app.admin.bootstrapUsername", "owner")
put(
"app.admin.bootstrapPasswordHash",
"\$argon2id\$v=19\$m=65536,t=3,p=1\$c2FsdHNhbHRzYWx0c2FsdA\$aGFzaGhhc2hoYXNoaGFzaGhhc2hoYXNoaGFzaA",
)
put("app.admin.bootstrapTotpSecretBase32", "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP")
}
val admin = AppConfig.from(config).admin
admin.enabled shouldBe true
admin.bootstrapEnabled shouldBe true
}
test("production accepts established admin without bootstrap credentials") {
val config = validProductionConfig().apply {
put("app.admin.enabled", "true")
put("app.admin.bootstrapEnabled", "false")
}
val admin = AppConfig.from(config).admin
admin.enabled shouldBe true
admin.bootstrapEnabled shouldBe false
admin.bootstrapTotpSecretBase32 shouldBe null
}
test("admin bootstrap cannot be enabled while admin routes are disabled") {
val config = validProductionConfig().apply {
put("app.admin.enabled", "false")
put("app.admin.bootstrapEnabled", "true")
}
shouldThrow<IllegalArgumentException> {
AppConfig.from(config)
}.message.orEmpty() shouldContain "bootstrapEnabled requires"
}
test("StoreKit requires an app identifier and dedicated credit product when enabled") {
val missingAppId = validProductionConfig().apply {
put("app.storeKit.enabled", "true")
put("app.storeKit.products", "500tks:500,1500tks:1500,3000tks:3000")
}
shouldThrow<IllegalArgumentException> {
AppConfig.from(missingAppId)
}.message.orEmpty() shouldContain "appAppleId is required"
val enabled = validProductionConfig().apply {
put("app.storeKit.enabled", "true")
put("app.storeKit.appAppleId", "6781553267")
put("app.storeKit.products", "500tks:500,1500tks:1500,3000tks:3000")
}
val storeKit = AppConfig.from(enabled).storeKit
storeKit.enabled shouldBe true
storeKit.appAppleId shouldBe 6_781_553_267
storeKit.products.map { it.productId to it.credits } shouldBe listOf(
"500tks" to 500,
"1500tks" to 1_500,
"3000tks" to 3_000,
)
}
test("StoreKit rejects duplicate product mappings") {
val config = validProductionConfig().apply {
put("app.storeKit.enabled", "true")
put("app.storeKit.appAppleId", "6781553267")
put("app.storeKit.products", "500tks:500,500tks:3000")
}
shouldThrow<ConfigValidationException> {
AppConfig.from(config)
}.message.orEmpty() shouldContain "duplicate product IDs"
}
test("production fails fast when Apple signing credentials are missing") {
val config = validProductionConfig().apply {
put("app.apple.keyId", "")
}
shouldThrow<ConfigValidationException> {
AppConfig.from(config)
}.message.orEmpty() shouldContain "app.apple.keyId"
}
test("production rejects monitor-only integrity configuration") {
val config = validProductionConfig().apply {
put("app.providers.volcengine.apiKey", "volcengine-key")
put("app.providers.deepseek.apiKey", "deepseek-key")
put("app.integrity.enforceDeviceCheck", "false")
put("app.integrity.enforceAppAttest", "false")
}
shouldThrow<IllegalArgumentException> {
AppConfig.from(config)
}.message.orEmpty() shouldContain "must enforce both DeviceCheck and App Attest"
}
test("production rejects provider endpoints outside the exact host allowlist") {
val config = validProductionConfig().apply {
put("app.providers.deepseek.endpoint", "https://127.0.0.1/v1")
}
shouldThrow<IllegalArgumentException> {
AppConfig.from(config)
}.message.orEmpty() shouldContain "DeepSeek endpoint"
}
test("production requires separate migration credentials") {
val missingMigrator = validProductionConfig().apply {
put("app.database.migrationUsername", "")
}
shouldThrow<ConfigValidationException> {
AppConfig.from(missingMigrator)
}.message.orEmpty() shouldContain "app.database.migrationUsername"
val reusedPassword = validProductionConfig().apply {
put("app.database.migrationPassword", "database-password")
}
shouldThrow<IllegalArgumentException> {
AppConfig.from(reusedPassword)
}.message.orEmpty() shouldContain "passwords must be distinct"
}
test("production requires independent cryptographic secrets") {
val config = validProductionConfig().apply {
put(
"app.antiAbuse.identityHmacKeyBase64",
Base64.getEncoder().encodeToString(ByteArray(32) { 7 }),
)
}
shouldThrow<IllegalArgumentException> {
AppConfig.from(config)
}.message.orEmpty() shouldContain "must be distinct"
}
test("production requires exact public and App Store URLs") {
val publicUrl = validProductionConfig().apply {
put("app.publicBaseUrl", "https://account.osglab.com.evil.example")
}
shouldThrow<IllegalArgumentException> {
AppConfig.from(publicUrl)
}.message.orEmpty() shouldContain "PUBLIC_BASE_URL"
val appStoreUrl = validProductionConfig().apply {
put("app.appStoreUrl", "https://apps.apple.com/app/id0000000000")
}
shouldThrow<IllegalArgumentException> {
AppConfig.from(appStoreUrl)
}.message.orEmpty() shouldContain "APP_STORE_URL"
}
})
private fun validConfig(environment: String) = MapApplicationConfig(
"app.environment" to environment,
"app.database.jdbcUrl" to "jdbc:mysql://localhost:3306/test",
"app.database.username" to "test",
"app.database.password" to "database-password",
"app.database.migrationUsername" to "test_migrator",
"app.database.migrationPassword" to "migration-password",
"app.database.maximumPoolSize" to "4",
"app.session.issuer" to "https://issuer.example",
"app.session.audience" to "ios-app",
"app.session.secret" to "01234567890123456789012345678901",
"app.session.accessMinutes" to "15",
"app.session.refreshDays" to "30",
"app.encryption.keyBase64" to Base64.getEncoder().encodeToString(ByteArray(32) { 7 }),
"app.antiAbuse.identityHmacKeyBase64" to
Base64.getEncoder().encodeToString(ByteArray(32) { 8 }),
"app.apple.clientId" to "com.example.app",
"app.apple.jwksUrl" to "https://appleid.apple.com/auth/keys",
"app.apple.tokenUrl" to "https://appleid.apple.com/auth/token",
"app.apple.revokeUrl" to "https://appleid.apple.com/auth/revoke",
"app.integrity.enforceDeviceCheck" to "false",
"app.integrity.enforceAppAttest" to "false",
)
private fun validProductionConfig() = validConfig("production").apply {
put("app.publicBaseUrl", "https://account.osglab.com")
put("app.inviteBaseUrl", "https://osglab.com/i")
put("app.appStoreUrl", "https://apps.apple.com/app/id1234567890")
put("app.apple.teamId", APP_ATTEST_TEAM_ID)
put("app.apple.keyId", "APPLE_KEY")
put("app.apple.clientId", APP_ATTEST_BUNDLE_ID)
put("app.apple.privateKeyPem", "private-key-material")
put("app.integrity.appleEnvironment", "production")
put("app.integrity.enforceDeviceCheck", "true")
put("app.integrity.enforceAppAttest", "true")
put("app.providers.volcengine.apiKey", "volcengine-key")
put("app.providers.deepseek.apiKey", "deepseek-key")
}