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=" openApi shouldContain "authorization_code_sha256=" openApi shouldContain "nonce_sha256=" 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}", )