Files
OSGAccountServer/src/test/kotlin/com/osglab/account/config/DeploymentConsistencyTest.kt
T
Rocky 4c9e5feec0
CI / verify (push) Has been cancelled
CI / publish (push) Has been cancelled
Make session refresh retries idempotent
Preserve the successor session for legitimate refresh retries so transient failures no longer revoke the user's session family.
2026-08-25 13:13:41 +08:00

462 lines
22 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("session refresh idempotency stays aligned across API, schema, and deployment") {
val openApi = root.read("docs/openapi.yaml")
val migration = root.read(
"src/main/resources/db/migration/V29__idempotent_session_refresh.sql",
)
openApi shouldContain "refreshOperationId"
migration shouldContain "encrypted_replacement_refresh_token"
migration shouldContain "refresh_replay_until"
listOf(
root.read("src/main/resources/application.yaml"),
root.read(".env.example"),
root.read("compose.yaml"),
).forEach { configuration ->
configuration shouldContain "LEGACY_REFRESH_REPLAY_SECONDS"
}
}
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("official content contract migration and runtime grants stay aligned") {
val openApi = root.read("docs/openapi.yaml")
val migration = root.read(
"src/main/resources/db/migration/V22__official_content_management.sql",
)
val hintFeedMigration = root.read(
"src/main/resources/db/migration/V24__hint_feed_generation.sql",
)
val privileges = root.read("docs/mysql-minimum-privileges.sql")
val smokePrivileges = root.read("deploy/smoke/runtime-grants.sql")
migration shouldContain "CREATE TABLE official_content_catalog"
migration shouldContain "CREATE TABLE official_skills"
migration shouldContain "CREATE TABLE official_skill_localizations"
migration shouldContain "CREATE TABLE official_hint_packs"
migration shouldContain "locale IN ('zh', 'en')"
hintFeedMigration shouldContain "CREATE TABLE hint_feed_settings"
hintFeedMigration shouldContain "CREATE TABLE hint_feed_generation_state"
hintFeedMigration shouldContain "generation_interval_hours BETWEEN 1 AND 168"
openApi shouldContain "schemaVersion: { type: integer, const: 1 }"
openApi shouldContain "pattern: \"^official\\\\."
openApi shouldContain "Cache-Control: { schema: { type: string, const: \"public,max-age=300\" } }"
val skillSchemas = openApi
.substringAfter(" SkillLocalization:")
.substringBefore(" AIHintCard:")
skillSchemas shouldContain "name: { type: string, minLength: 1, maxLength: 40 }"
skillSchemas shouldContain "summary: { type: string, minLength: 1, maxLength: 200 }"
skillSchemas shouldContain "prompt: { type: string, minLength: 1, maxLength: 6000 }"
skillSchemas shouldContain "systemImage: { type: string, minLength: 1, maxLength: 100 }"
skillSchemas shouldContain "sortOrder: { type: integer, minimum: 0, maximum: 100000 }"
skillSchemas shouldContain "maxItems: 100"
listOf(privileges, smokePrivileges).forEach { grants ->
grants shouldContain "SELECT ON osg_account"
grants shouldContain "official_content_catalog"
grants shouldContain "official_skills"
grants shouldContain "official_skill_localizations"
grants shouldContain "official_hint_packs"
grants shouldContain "hint_feed_settings"
grants shouldContain "hint_feed_generation_state"
}
}
test("admin ledger operations stay indexed exact and privacy minimized") {
val migration = root.read(
"src/main/resources/db/migration/V19__admin_ledger_operations.sql",
)
migration shouldContain
"MODIFY reference_id CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL"
migration shouldContain
"MODIFY reservation_id CHAR(36) CHARACTER SET ascii COLLATE ascii_bin NULL"
migration shouldContain "ON credit_ledger (amount_delta, id)"
migration shouldContain "ON credit_ledger (user_id, amount_delta, id)"
migration shouldContain "ON provider_requests (capability, request_source, reservation_id)"
migration shouldContain "ON provider_requests (request_source, capability, reservation_id)"
val openApi = root.read("docs/openapi.yaml")
val ledgerSchema = openApi
.substringAfter(" LedgerEntryType:")
.substringBefore(" AdminLedgerPage:")
LEDGER_ENTRY_TYPE_NAMES.forEach(ledgerSchema::shouldContain)
ledgerSchema shouldContain "deprecated: true"
ledgerSchema shouldContain "referenceId"
ledgerSchema shouldContain "reservationId"
ledgerSchema shouldContain
"required: [entryId, userId, type, entryType, amount, balanceAfter, reasonCode, createdAt]"
ledgerSchema shouldNotContain "enum: [grant, reserve, settle, refund, adjustment]"
ledgerSchema shouldNotContain "idempotencyKey"
ledgerSchema shouldNotContain "appAccountToken"
ledgerSchema shouldNotContain "signedTransaction"
ledgerSchema shouldNotContain "appleSubject"
}
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("StoreKit history remains ledger backed, indexed, and privacy minimized") {
val historyIndex = root.read(
"src/main/resources/db/migration/V15__storekit_purchase_history_index.sql",
)
historyIndex shouldContain
"ON storekit_credit_purchases (user_id, purchased_at, transaction_id)"
val openApi = root.read("docs/openapi.yaml")
val historySchema = openApi
.substringAfter(" StoreKitTransactionHistoryItem:")
.substringBefore(" AppleAppSiteAssociation:")
historySchema shouldContain "purchasedAt"
historySchema shouldContain "status"
historySchema shouldContain "nextCursor"
historySchema shouldNotContain "signedTransaction"
historySchema shouldNotContain "appAccountToken"
historySchema shouldNotContain "userId"
historySchema shouldNotContain "accountId"
}
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("product analytics contract stays allowlisted and account deletions cascade") {
val migration = root.read(
"src/main/resources/db/migration/V16__product_analytics_events.sql",
)
migration shouldContain "product_analytics_installations"
migration shouldContain "product_analytics_events"
migration shouldContain "REFERENCES accounts (id) ON DELETE CASCADE"
migration shouldContain "PRIMARY KEY (installation_hash, client_event_id)"
val openApi = root.read("docs/openapi.yaml")
val eventSchema = openApi
.substringAfter(" ProductAnalyticsEvent:")
.substringBefore(" SkillLocalization:")
eventSchema shouldContain "additionalProperties: false"
eventSchema shouldContain "AI_FEATURE_SUCCEEDED"
eventSchema shouldContain "INSUFFICIENT_CREDITS"
eventSchema shouldNotContain "prompt"
eventSchema shouldNotContain "transcript"
eventSchema shouldNotContain "audio"
eventSchema shouldNotContain "modelOutput"
openApi shouldContain "schema: { \$ref: \"#/components/schemas/AdminProductAnalytics\" }"
val keyboardMigration = root.read(
"src/main/resources/db/migration/V23__keyboard_usage_daily_summaries.sql",
)
keyboardMigration shouldContain "keyboard_usage_daily_summaries"
keyboardMigration shouldContain
"UNIQUE KEY uq_keyboard_usage_installation_date (installation_hash, summary_date)"
keyboardMigration shouldContain "ON DELETE CASCADE"
keyboardMigration shouldNotContain "input_text"
keyboardMigration shouldNotContain "host_application"
val keyboardSchema = openApi
.substringAfter(" KeyboardUsageSummary:")
.substringBefore(" ProductAnalyticsBatchResponse:")
keyboardSchema shouldContain "additionalProperties: false"
keyboardSchema shouldContain "chineseCharacterCount"
keyboardSchema shouldContain "mixedLanguageSessionCount"
keyboardSchema shouldNotContain "userText"
keyboardSchema shouldNotContain "keystrokes"
keyboardSchema shouldNotContain "hostApplication"
}
test("anonymous OOBE feature claims are scoped to each guided session") {
val migration = root.read(
"src/main/resources/db/migration/V27__scope_oobe_claims_to_grant.sql",
)
val openApi = root.read("docs/openapi.yaml")
migration shouldContain "PRIMARY KEY (grant_id, feature)"
migration shouldContain
"FOREIGN KEY (grant_id) REFERENCES oobe_gateway_grants (id) ON DELETE CASCADE"
migration shouldNotContain "PRIMARY KEY (subject_id, feature)"
openApi shouldContain "Each feature can"
openApi shouldContain "succeed once within this short-lived grant"
}
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}"
compose shouldContain "ADMIN_MTLS_REQUIRED: \${ADMIN_MTLS_REQUIRED:-true}"
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.gateway_complimentary_requests"
privileges shouldContain
"GRANT INSERT, UPDATE, DELETE ON osg_account.gateway_complimentary_requests"
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 X-OSG-mTLS-Verified \$ssl_client_verify;"
openResty shouldNotContain "proxy_set_header X-OSG-mTLS-Verified \"SUCCESS\";"
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 LEDGER_ENTRY_TYPE_NAMES = listOf(
"SIGNUP_TRIAL",
"MANUAL_GRANT",
"USAGE_RESERVE",
"USAGE_SETTLE",
"USAGE_RELEASE",
"USAGE_REFUND",
"REFERRAL_INVITER",
"REFERRAL_INVITEE",
"STOREKIT_PURCHASE",
"SUBSCRIPTION_GRANT",
)
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/analytics/events",
"/v1/analytics/keyboard-usage",
"/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/oobe/grants",
"/v1/oobe/grants/refresh",
"/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/content/skills",
"/v1/content/hints/manifest",
"/v1/content/hints/{locale}",
"/hints/manifest.json",
"/hints/hints-{locale}.json",
"/v1/admin/content/skills",
"/v1/admin/content/skills/{id}",
"/v1/admin/content/skills/{id}/enable",
"/v1/admin/content/skills/{id}/disable",
"/v1/admin/content/hints/generation/settings",
"/v1/admin/content/hints/generation/status",
"/v1/admin/content/hints/generation/regenerate",
"/v1/admin/content/hints/{locale}",
"/v1/admin/auth/session",
"/v1/admin/auth/login",
"/v1/admin/auth/logout",
"/v1/admin/providers",
"/v1/admin/providers/{providerId}/api-key",
"/v1/admin/providers/{providerId}/api-key/reveal",
"/v1/admin/overview",
"/v1/admin/referrals",
"/v1/admin/analytics",
"/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}",
)