75c046d91d
Expose a paginated global ledger timeline and load it automatically so operators can see recent credit activity without first locating a user.
255 lines
12 KiB
Kotlin
255 lines
12 KiB
Kotlin
package com.osglab.account.config
|
|
|
|
import io.kotest.core.spec.style.FunSpec
|
|
import io.kotest.matchers.shouldBe
|
|
import io.kotest.matchers.string.shouldContain
|
|
import io.kotest.matchers.string.shouldNotContain
|
|
import java.nio.file.Files
|
|
import java.nio.file.Path
|
|
|
|
class DeploymentConsistencyTest : FunSpec({
|
|
val root = Path.of(System.getProperty("user.dir"))
|
|
|
|
test("OpenAPI documents every mounted API route") {
|
|
val openApi = root.read("docs/openapi.yaml")
|
|
val documentedPaths = Regex("""(?m)^ (/[^:]+):\s*$""")
|
|
.findAll(openApi)
|
|
.map { it.groupValues[1] }
|
|
.toSet()
|
|
|
|
documentedPaths shouldBe EXPECTED_PUBLIC_PATHS
|
|
}
|
|
|
|
test("OpenAPI defines admin pagination and response contracts") {
|
|
val openApi = root.read("docs/openapi.yaml")
|
|
val sessionSchema = openApi
|
|
.substringAfter(" AdminSessionState:")
|
|
.substringBefore(" AdminLoginResponse:")
|
|
|
|
sessionSchema shouldNotContain "csrfToken"
|
|
openApi shouldContain "schema: { \$ref: \"#/components/schemas/AdminOverview\" }"
|
|
openApi shouldContain "schema: { \$ref: \"#/components/schemas/AdminReferralOverview\" }"
|
|
openApi shouldContain "schema: { \$ref: \"#/components/schemas/AdminUserPage\" }"
|
|
openApi shouldContain "schema: { \$ref: \"#/components/schemas/AdminUserDetail\" }"
|
|
openApi shouldContain "schema: { \$ref: \"#/components/schemas/AdminLedgerPage\" }"
|
|
openApi shouldContain "schema: { \$ref: \"#/components/schemas/AdminAuditPage\" }"
|
|
openApi shouldContain "pendingBindings"
|
|
openApi shouldContain "ineligibleBindings"
|
|
openApi shouldContain "chargedCredits"
|
|
openApi shouldContain "referralCode"
|
|
}
|
|
|
|
test("provider defaults and Apple integrity contract stay production compatible") {
|
|
val providerConfigurations = listOf(
|
|
root.read("src/main/kotlin/com/osglab/account/config/AppConfig.kt"),
|
|
root.read("src/main/resources/application.yaml"),
|
|
root.read(".env.example"),
|
|
root.read("compose.yaml"),
|
|
)
|
|
providerConfigurations.forEach { configuration ->
|
|
configuration shouldContain "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel"
|
|
configuration shouldNotContain "bigmodel_async"
|
|
}
|
|
|
|
val openApi = root.read("docs/openapi.yaml")
|
|
openApi shouldContain "osg-app-attest-v1"
|
|
openApi shouldContain "purpose=apple-sign-in"
|
|
openApi shouldContain "identity_token_sha256=<identity-token-digest>"
|
|
openApi shouldContain "authorization_code_sha256=<authorization-code-digest>"
|
|
openApi shouldContain "nonce_sha256=<raw-nonce-digest>"
|
|
openApi shouldContain "including the final line feed"
|
|
openApi shouldContain "unpadded Base64URL"
|
|
}
|
|
|
|
test("StoreKit catalog and smaller immutable rates stay aligned") {
|
|
listOf(root.read(".env.example"), root.read("compose.yaml")).forEach { configuration ->
|
|
configuration shouldContain "STOREKIT_PRODUCTS"
|
|
configuration shouldContain "500tks:500,1500tks:1500,3000tks:3000"
|
|
configuration shouldNotContain "STOREKIT_PRODUCT_CREDITS"
|
|
configuration shouldContain "SIGNUP_TRIAL_CREDITS"
|
|
configuration shouldContain "REFERRAL_INVITER_CREDITS"
|
|
}
|
|
|
|
val rates = root.read("src/main/resources/db/migration/V10__smaller_credit_units.sql")
|
|
rates shouldContain "'10000000-0000-0000-0000-000000000003'"
|
|
rates shouldContain "'10000000-0000-0000-0000-000000000004'"
|
|
rates shouldContain "1,\n 3000,"
|
|
rates shouldContain "1,\n 1000,\n 1,\n 400,"
|
|
}
|
|
|
|
test("account profiles cascade on deletion and grants stay aligned") {
|
|
val profileMigration = root.read(
|
|
"src/main/resources/db/migration/V11__account_profiles.sql",
|
|
)
|
|
val referralMigration = root.read(
|
|
"src/main/resources/db/migration/V12__align_referral_rewards.sql",
|
|
)
|
|
val ledgerTimelineMigration = root.read(
|
|
"src/main/resources/db/migration/V13__credit_ledger_global_timeline.sql",
|
|
)
|
|
profileMigration shouldContain "encrypted_display_name MEDIUMTEXT NOT NULL"
|
|
profileMigration shouldContain "REFERENCES accounts (id) ON DELETE CASCADE"
|
|
referralMigration shouldContain "inviter_reward_credits = 1000"
|
|
referralMigration shouldContain "invitee_reward_credits = 1000"
|
|
ledgerTimelineMigration shouldContain "ON credit_ledger (created_at, id)"
|
|
|
|
listOf(
|
|
root.read("src/main/resources/application.yaml"),
|
|
root.read(".env.example"),
|
|
root.read("compose.yaml"),
|
|
).forEach { configuration ->
|
|
configuration shouldContain "1000"
|
|
configuration shouldNotContain "SIGNUP_TRIAL_CREDITS=334"
|
|
}
|
|
}
|
|
|
|
test("production Compose reuses private MySQL and hardens the application container") {
|
|
val compose = root.read("compose.yaml")
|
|
|
|
compose shouldContain "127.0.0.1:\${ACCOUNT_BIND_PORT:-18080}:8080"
|
|
compose shouldContain "ghcr.io/hkgood/osg-account-server"
|
|
compose shouldContain "pull_policy: always"
|
|
compose shouldContain "external: true"
|
|
compose shouldContain "account-egress:"
|
|
compose shouldContain "user: \"10001:10001\""
|
|
compose shouldContain "read_only: true"
|
|
compose shouldContain "cap_drop:"
|
|
compose shouldContain "no-new-privileges:true"
|
|
compose shouldNotContain "image: mysql"
|
|
compose shouldNotContain "3306:3306"
|
|
compose shouldNotContain "0.0.0.0:"
|
|
}
|
|
|
|
test("admin bootstrap is one-time and runtime database grants stay explicit") {
|
|
val compose = root.read("compose.yaml")
|
|
val privileges = root.read("docs/mysql-minimum-privileges.sql")
|
|
val smokePrivileges = root.read("deploy/smoke/runtime-grants.sql")
|
|
|
|
compose shouldContain "ADMIN_BOOTSTRAP_ENABLED: \${ADMIN_BOOTSTRAP_ENABLED:-false}"
|
|
privileges shouldContain "GRANT SELECT ON osg_account.admin_operators"
|
|
privileges shouldContain "GRANT INSERT, UPDATE ON osg_account.admin_operators"
|
|
privileges shouldContain "GRANT SELECT ON osg_account.admin_sessions"
|
|
privileges shouldContain "GRANT INSERT, UPDATE, DELETE ON osg_account.admin_sessions"
|
|
privileges shouldContain "GRANT SELECT ON osg_account.gateway_grant_scopes"
|
|
privileges shouldContain "GRANT INSERT ON osg_account.gateway_grant_scopes"
|
|
privileges shouldContain "GRANT SELECT ON osg_account.gateway_refresh_tokens"
|
|
privileges shouldContain "GRANT INSERT, UPDATE ON osg_account.gateway_refresh_tokens"
|
|
privileges shouldContain "GRANT SELECT ON osg_account.account_profiles"
|
|
privileges shouldContain "GRANT INSERT, UPDATE ON osg_account.account_profiles"
|
|
privileges shouldContain "GRANT INSERT ON osg_account.admin_audit_log"
|
|
privileges shouldContain "GRANT INSERT ON osg_account.admin_credit_grants"
|
|
privileges shouldContain "GRANT SELECT ON osg_account.storekit_credit_purchases"
|
|
privileges shouldContain "GRANT INSERT ON osg_account.storekit_credit_purchases"
|
|
privileges shouldNotContain "UPDATE ON osg_account.admin_audit_log"
|
|
privileges shouldNotContain "DELETE ON osg_account.admin_credit_grants"
|
|
smokePrivileges shouldContain "GRANT SELECT ON osg_account_smoke.account_profiles"
|
|
smokePrivileges shouldContain "GRANT INSERT, UPDATE ON osg_account_smoke.account_profiles"
|
|
}
|
|
|
|
test("container image remains non-root and read-only compatible") {
|
|
val dockerfile = root.read("Dockerfile")
|
|
val build = root.read("build.gradle.kts")
|
|
|
|
dockerfile shouldContain "USER 10001:10001"
|
|
dockerfile shouldContain "ENV HOME=/tmp"
|
|
dockerfile shouldContain "http://127.0.0.1:8080/health/ready"
|
|
dockerfile shouldNotContain "ENTRYPOINT [\"sh\""
|
|
dockerfile shouldNotContain "jansi.tmpdir"
|
|
build shouldContain "configurations.configureEach"
|
|
build shouldContain "exclude(group = \"org.fusesource.jansi\", module = \"jansi\")"
|
|
build shouldContain "tasks.register(\"verifyRuntimeClasspath\")"
|
|
}
|
|
|
|
test("OpenResty proxies HTTP WebSocket invitations and both AASA paths safely") {
|
|
val openResty = root.read("deploy/openresty-account.conf")
|
|
|
|
openResty shouldContain "proxy_set_header Upgrade \$http_upgrade;"
|
|
openResty shouldContain "proxy_set_header Connection \$connection_upgrade;"
|
|
openResty shouldContain "location = /.well-known/apple-app-site-association"
|
|
openResty shouldContain "location = /apple-app-site-association"
|
|
openResty shouldContain "location ^~ /i/"
|
|
Regex("""location \^~ /i/ \{\s+access_log off;""").containsMatchIn(openResty) shouldBe true
|
|
openResty shouldNotContain "alias /www/wwwroot/osglab.com/apple-app-site-association"
|
|
openResty shouldNotContain "unsafe-inline"
|
|
}
|
|
|
|
test("CI definition is singular and leaves MySQL lifecycle to Testcontainers") {
|
|
val ci = root.read(".github/workflows/ci.yml")
|
|
|
|
Regex("""(?m)^name: CI$""").findAll(ci).count() shouldBe 1
|
|
Regex("""(?m)^jobs:$""").findAll(ci).count() shouldBe 1
|
|
ci shouldContain "docker compose -f compose.yaml config --quiet"
|
|
ci shouldContain "./gradlew --no-daemon clean test"
|
|
ci shouldContain "./gradlew --no-daemon buildFatJar"
|
|
ci shouldContain "docker/build-push-action@v6"
|
|
ci shouldContain "packages: write"
|
|
ci shouldNotContain "3306:3306"
|
|
ci shouldNotContain "TEST_DB_"
|
|
}
|
|
|
|
test("AASA has one runtime template and no deploy-time identifier placeholder") {
|
|
val aasa = root.read("src/main/resources/invite/apple-app-site-association.json")
|
|
|
|
aasa shouldContain "\"{{APPLE_APP_ID}}\""
|
|
aasa shouldContain "\"/i/*\""
|
|
Files.exists(root.resolve("deploy/apple-app-site-association")) shouldBe false
|
|
}
|
|
})
|
|
|
|
private fun Path.read(relativePath: String): String =
|
|
Files.readString(resolve(relativePath))
|
|
|
|
private val EXPECTED_PUBLIC_PATHS = setOf(
|
|
"/health",
|
|
"/health/live",
|
|
"/health/ready",
|
|
"/v1/auth/apple",
|
|
"/v1/auth/refresh",
|
|
"/v1/auth/logout",
|
|
"/v1/account",
|
|
"/v1/apple/events",
|
|
"/v1/credits/balance",
|
|
"/v1/credits/ledger",
|
|
"/v1/credits/rates",
|
|
"/v1/storekit/products",
|
|
"/v1/storekit/transactions",
|
|
"/v1/referrals",
|
|
"/v1/referrals/me",
|
|
"/v1/referrals/code",
|
|
"/v1/referrals/redeem",
|
|
"/v1/referrals/bind",
|
|
"/v1/referrals/campaigns",
|
|
"/v1/integrity/challenges",
|
|
"/v1/integrity/attest",
|
|
"/v1/integrity/assert",
|
|
"/v1/gateway/catalog",
|
|
"/v1/gateway/grants",
|
|
"/v1/gateway/grants/refresh",
|
|
"/v1/gateway/grants/{grantId}",
|
|
"/v1/gateway/llm/{capability}",
|
|
"/v1/gateway/asr",
|
|
"/v1/gateway/asr/sessions",
|
|
"/v1/gateway/asr/sessions/{sessionId}/stream",
|
|
"/v1/admin/auth/session",
|
|
"/v1/admin/auth/login",
|
|
"/v1/admin/auth/logout",
|
|
"/v1/admin/overview",
|
|
"/v1/admin/referrals",
|
|
"/v1/admin/users",
|
|
"/v1/admin/users/{userId}",
|
|
"/v1/admin/users/{userId}/ledger",
|
|
"/v1/admin/credits/ledger",
|
|
"/v1/admin/credits/grants",
|
|
"/v1/admin/operators/summary",
|
|
"/v1/admin/operators",
|
|
"/v1/admin/operators/{operatorId}/enable",
|
|
"/v1/admin/operators/{operatorId}/disable",
|
|
"/v1/admin/operators/{operatorId}/unlock",
|
|
"/v1/admin/operators/{operatorId}/credentials/reset",
|
|
"/v1/admin/operators/{operatorId}/sessions/revoke",
|
|
"/v1/admin/audit",
|
|
"/.well-known/apple-app-site-association",
|
|
"/apple-app-site-association",
|
|
"/i/{code}",
|
|
)
|