forked from wangziqi/gongxue-base
1011 lines
41 KiB
JavaScript
1011 lines
41 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { spawnSync } from 'node:child_process';
|
|
|
|
const repoRoot = process.cwd();
|
|
const scriptPath = path.join(repoRoot, 'scripts', 'production-readiness-check.js');
|
|
|
|
function runReadiness(envContent, options = {}) {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tiku-readiness-'));
|
|
const envFile = path.join(tempDir, '.env');
|
|
fs.writeFileSync(envFile, envContent, 'utf8');
|
|
const args = [scriptPath, '--env-file', envFile, '--skip-db', '--json'];
|
|
if (options.providerRows) {
|
|
const fixtureFile = path.join(tempDir, 'provider-fixture.json');
|
|
fs.writeFileSync(fixtureFile, JSON.stringify({ rows: options.providerRows }, null, 2), 'utf8');
|
|
args.push('--provider-config-fixture', fixtureFile);
|
|
}
|
|
if (options.tenantRows) {
|
|
const fixtureFile = path.join(tempDir, 'tenant-fixture.json');
|
|
fs.writeFileSync(fixtureFile, JSON.stringify({ rows: options.tenantRows }, null, 2), 'utf8');
|
|
args.push('--tenant-config-fixture', fixtureFile);
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(options, 'environmentSafetyRow')) {
|
|
const fixtureFile = path.join(tempDir, 'environment-safety-fixture.json');
|
|
fs.writeFileSync(
|
|
fixtureFile,
|
|
JSON.stringify({ row: options.environmentSafetyRow }, null, 2),
|
|
'utf8',
|
|
);
|
|
args.push('--environment-safety-fixture', fixtureFile);
|
|
}
|
|
if (Object.prototype.hasOwnProperty.call(options, 'migrationHistoryRow')) {
|
|
const fixtureFile = path.join(tempDir, 'migration-history-fixture.json');
|
|
fs.writeFileSync(
|
|
fixtureFile,
|
|
JSON.stringify({ row: options.migrationHistoryRow }, null, 2),
|
|
'utf8',
|
|
);
|
|
args.push('--migration-history-fixture', fixtureFile);
|
|
}
|
|
|
|
const result = spawnSync(process.execPath, args, {
|
|
cwd: repoRoot,
|
|
encoding: 'utf8',
|
|
env: {
|
|
PATH: process.env.PATH || '',
|
|
Path: process.env.Path || '',
|
|
SystemRoot: process.env.SystemRoot || '',
|
|
ComSpec: process.env.ComSpec || '',
|
|
TEMP: process.env.TEMP || os.tmpdir(),
|
|
TMP: process.env.TMP || os.tmpdir(),
|
|
},
|
|
});
|
|
|
|
const payload = JSON.parse(result.stdout || '{}');
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
return { ...result, payload };
|
|
}
|
|
|
|
function productionEnv(overrides = '') {
|
|
return `
|
|
NODE_ENV=production
|
|
${overrides}
|
|
DATABASE_URL=postgresql://tiku_api:prod_password@db.prod.internal:5432/tiku
|
|
DB_EXPECTED_RUNTIME_ROLE=tiku_api
|
|
CORS_ORIGIN=https://platform-admin.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
|
AUTH_CODE_PEPPER=s3cure-prod-code-pepper-2026-06-29-abcdef
|
|
AUTH_SESSION_SECRET=s3cure-prod-session-secret-2026-06-29-ghijkl
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=s3cure-platform-admin-key-2026-06-29-mnopqr
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
`;
|
|
}
|
|
|
|
const unsafe = runReadiness(`
|
|
NODE_ENV=development
|
|
DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:54322/postgres
|
|
AUTH_SMS_PROVIDER=mock
|
|
`);
|
|
|
|
assert.notEqual(unsafe.status, 0, 'unsafe production readiness should fail');
|
|
assert.ok(unsafe.payload.summary?.blocker > 0, 'unsafe readiness should report blockers');
|
|
assert.ok(
|
|
unsafe.payload.checks?.some(item => item.id === 'env.node_env' && item.status === 'blocker'),
|
|
'unsafe readiness should block non-production NODE_ENV',
|
|
);
|
|
assert.ok(
|
|
unsafe.payload.checks?.some(item => item.id === 'env.auth_sms_provider' && item.status === 'blocker'),
|
|
'unsafe readiness should block mock SMS provider',
|
|
);
|
|
|
|
const unsupportedSmsProvider = runReadiness(`
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
|
CORS_ORIGIN=https://student.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun-production
|
|
AUTH_CODE_PEPPER=s3cure-prod-code-pepper-2026-06-29-abcdef
|
|
AUTH_SESSION_SECRET=s3cure-prod-session-secret-2026-06-29-ghijkl
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=s3cure-platform-admin-key-2026-06-29-mnopqr
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
`);
|
|
|
|
assert.notEqual(unsupportedSmsProvider.status, 0, 'unsupported production SMS provider readiness should fail');
|
|
assert.ok(
|
|
unsupportedSmsProvider.payload.checks?.some(item => item.id === 'env.auth_sms_provider' && item.status === 'blocker'),
|
|
'readiness should block unsupported AUTH_SMS_PROVIDER values',
|
|
);
|
|
|
|
const traditionalAliyunSmsProvider = runReadiness(`
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
|
CORS_ORIGIN=https://student.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun
|
|
AUTH_CODE_PEPPER=s3cure-prod-code-pepper-2026-06-29-abcdef
|
|
AUTH_SESSION_SECRET=s3cure-prod-session-secret-2026-06-29-ghijkl
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=s3cure-platform-admin-key-2026-06-29-mnopqr
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
`);
|
|
|
|
assert.notEqual(traditionalAliyunSmsProvider.status, 0, 'traditional Aliyun SMS provider readiness should fail in production');
|
|
assert.ok(
|
|
traditionalAliyunSmsProvider.payload.checks?.some(item => item.id === 'env.auth_sms_provider' && item.status === 'blocker'),
|
|
'readiness should require PNVS instead of traditional Aliyun SMS',
|
|
);
|
|
|
|
const strongSecretA = 's3cure-prod-code-pepper-2026-06-29-abcdef';
|
|
const strongSecretB = 's3cure-prod-session-secret-2026-06-29-ghijkl';
|
|
const strongSecretC = 's3cure-platform-admin-key-2026-06-29-mnopqr';
|
|
|
|
const safe = runReadiness(`
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
|
CORS_ORIGIN=https://student.gongxue100.com,https://tenant-admin.gongxue100.com,https://platform-admin.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
|
AUTH_CODE_PEPPER=${strongSecretA}
|
|
AUTH_SESSION_SECRET=${strongSecretB}
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=${strongSecretC}
|
|
MAX_JSON_BODY_BYTES=1048576
|
|
MAX_IMPORT_JSON_BODY_BYTES=10485760
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
STORAGE_ALLOWED_MIME_TYPES=application/pdf,image/png,image/jpeg,video/mp4,text/plain,text/csv
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TIMEOUT_MS=10000
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
WORKER_CRM_ALLOW_INSECURE_LOCALHOST=false
|
|
WORKER_PLATFORM_AUDIT_NOTIFICATION_ALLOW_INSECURE_LOCALHOST=false
|
|
WORKER_PLATFORM_DUNNING_NOTIFICATION_ALLOW_INSECURE_LOCALHOST=false
|
|
WORKER_CRM_BATCH_SIZE=20
|
|
WORKER_CRM_POLL_INTERVAL_MS=10000
|
|
WORKER_COMMERCE_BATCH_SIZE=20
|
|
WORKER_COMMERCE_POLL_INTERVAL_MS=30000
|
|
WORKER_PROVIDER_BILL_POLL_INTERVAL_MS=60000
|
|
WORKER_PLATFORM_DUNNING_NOTIFICATION_POLL_INTERVAL_MS=30000
|
|
WORKER_PLATFORM_AUDIT_NOTIFICATION_POLL_INTERVAL_MS=30000
|
|
WORKER_ASSET_BATCH_SIZE=50
|
|
WORKER_ASSET_POLL_INTERVAL_MS=30000
|
|
WORKER_IMPORT_BATCH_SIZE=5
|
|
WORKER_IMPORT_POLL_INTERVAL_MS=10000
|
|
WORKER_IMPORT_LEASE_SECONDS=120
|
|
WORKER_IMPORT_HEARTBEAT_INTERVAL_MS=30000
|
|
WORKER_PUBLIC_BANK_SYNC_BATCH_SIZE=5
|
|
WORKER_PUBLIC_BANK_SYNC_POLL_INTERVAL_MS=60000
|
|
WORKER_EXPORT_POLL_INTERVAL_MS=10000
|
|
`);
|
|
|
|
assert.equal(safe.status, 0, `safe readiness should pass without blockers: ${safe.stdout} ${safe.stderr}`);
|
|
assert.equal(safe.payload.summary?.blocker, 0, 'safe readiness should have no blockers');
|
|
assert.ok(
|
|
safe.payload.checks?.some(item => item.id === 'db.skipped' && item.status === 'warn'),
|
|
'env-only readiness should explicitly warn that DB checks are skipped',
|
|
);
|
|
|
|
const migrationFiles = fs.readdirSync(path.join(repoRoot, 'supabase', 'migrations'))
|
|
.filter(file => file.endsWith('.sql'))
|
|
.sort();
|
|
const latestMigrationVersion = /^(\d+)_/.exec(migrationFiles.at(-1) || '')?.[1];
|
|
assert.ok(latestMigrationVersion, 'repository must have a latest numeric Supabase migration');
|
|
|
|
const currentMigrationHistory = runReadiness(productionEnv(), {
|
|
migrationHistoryRow: {
|
|
latestVersion: latestMigrationVersion,
|
|
appliedCount: migrationFiles.length,
|
|
distinctVersionCount: migrationFiles.length,
|
|
expectedVersionApplied: true,
|
|
},
|
|
});
|
|
assert.equal(currentMigrationHistory.status, 0, 'current migration history fixture should pass');
|
|
assert.ok(
|
|
currentMigrationHistory.payload.checks?.some(item => (
|
|
item.id === 'db.migrations.current' && item.status === 'pass'
|
|
)),
|
|
'readiness should pass migration history that includes the repository latest version',
|
|
);
|
|
|
|
const staleMigrationHistory = runReadiness(productionEnv(), {
|
|
migrationHistoryRow: {
|
|
latestVersion: '202607120018',
|
|
appliedCount: migrationFiles.length - 1,
|
|
distinctVersionCount: migrationFiles.length - 1,
|
|
expectedVersionApplied: false,
|
|
},
|
|
});
|
|
assert.notEqual(staleMigrationHistory.status, 0, 'stale migration history must fail readiness');
|
|
assert.ok(
|
|
staleMigrationHistory.payload.checks?.some(item => (
|
|
item.id === 'db.migrations.current' && item.status === 'blocker'
|
|
)),
|
|
'readiness should block a database missing the repository latest migration',
|
|
);
|
|
|
|
const inconsistentMigrationHistory = runReadiness(productionEnv(), {
|
|
migrationHistoryRow: {
|
|
latestVersion: latestMigrationVersion,
|
|
appliedCount: migrationFiles.length + 1,
|
|
distinctVersionCount: migrationFiles.length,
|
|
expectedVersionApplied: true,
|
|
},
|
|
});
|
|
assert.notEqual(inconsistentMigrationHistory.status, 0, 'duplicate migration history must fail readiness');
|
|
|
|
const truncatedMigrationHistory = runReadiness(productionEnv(), {
|
|
migrationHistoryRow: {
|
|
latestVersion: latestMigrationVersion,
|
|
appliedCount: migrationFiles.length - 1,
|
|
distinctVersionCount: migrationFiles.length - 1,
|
|
expectedVersionApplied: true,
|
|
},
|
|
});
|
|
assert.notEqual(
|
|
truncatedMigrationHistory.status,
|
|
0,
|
|
'migration history shorter than the repository migration set must fail readiness',
|
|
);
|
|
|
|
const dynamicTenantCorsDisabled = runReadiness(productionEnv('CORS_TENANT_DOMAINS_ENABLED=false'));
|
|
assert.notEqual(dynamicTenantCorsDisabled.status, 0, 'disabled tenant-domain CORS must fail production readiness');
|
|
assert.ok(
|
|
dynamicTenantCorsDisabled.payload.checks?.some(item => (
|
|
item.id === 'env.cors_tenant_domains_enabled' && item.status === 'blocker'
|
|
)),
|
|
'readiness must require dynamic tenant-domain CORS in production',
|
|
);
|
|
|
|
for (const [override, checkId] of [
|
|
['WORKER_IMPORT_LEASE_SECONDS=9', 'env.worker_import_lease_seconds'],
|
|
[
|
|
'WORKER_IMPORT_LEASE_SECONDS=60\nWORKER_IMPORT_HEARTBEAT_INTERVAL_MS=30000',
|
|
'env.worker_import_heartbeat_interval_ms',
|
|
],
|
|
]) {
|
|
const result = runReadiness(productionEnv(override));
|
|
assert.notEqual(result.status, 0, `${checkId} should fail production readiness`);
|
|
assert.ok(
|
|
result.payload.checks?.some(item => item.id === checkId && item.status === 'blocker'),
|
|
`${checkId} should be reported as a blocker`,
|
|
);
|
|
}
|
|
|
|
for (const [key, unsafeValue] of [
|
|
['CORS_TENANT_DOMAIN_CACHE_TTL_MS', '999'],
|
|
['CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS', '300001'],
|
|
['CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES', '99'],
|
|
]) {
|
|
const result = runReadiness(productionEnv(`${key}=${unsafeValue}`));
|
|
assert.notEqual(result.status, 0, `${key} outside the production range must fail readiness`);
|
|
assert.ok(
|
|
result.payload.checks?.some(item => (
|
|
item.id === `env.${key.toLowerCase()}` && item.status === 'blocker'
|
|
)),
|
|
`readiness must block unsafe ${key}`,
|
|
);
|
|
}
|
|
|
|
const missingExpectedRole = runReadiness(productionEnv('DB_EXPECTED_RUNTIME_ROLE='));
|
|
assert.equal(
|
|
missingExpectedRole.status,
|
|
0,
|
|
'env-only readiness should defer the authoritative runtime-role identity check to --check-db',
|
|
);
|
|
assert.ok(
|
|
missingExpectedRole.payload.checks?.some(item => (
|
|
item.id === 'env.db_expected_runtime_role' && item.status === 'warn'
|
|
)),
|
|
'env-only readiness should warn when DB_EXPECTED_RUNTIME_ROLE is absent',
|
|
);
|
|
|
|
const mismatchedExpectedRole = runReadiness(productionEnv('DB_EXPECTED_RUNTIME_ROLE=tiku_worker'));
|
|
assert.notEqual(mismatchedExpectedRole.status, 0, 'DATABASE_URL role mismatch must fail readiness');
|
|
assert.ok(
|
|
mismatchedExpectedRole.payload.checks?.some(item => (
|
|
item.id === 'env.database_runtime_role' && item.status === 'blocker'
|
|
)),
|
|
'readiness must block a DATABASE_URL username that differs from DB_EXPECTED_RUNTIME_ROLE',
|
|
);
|
|
|
|
const invalidWorkerPollInterval = runReadiness(`
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
|
CORS_ORIGIN=https://student.gongxue100.com,https://tenant-admin.gongxue100.com,https://platform-admin.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
|
AUTH_CODE_PEPPER=${strongSecretA}
|
|
AUTH_SESSION_SECRET=${strongSecretB}
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=${strongSecretC}
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
WORKER_CRM_POLL_INTERVAL_MS=0
|
|
`);
|
|
|
|
assert.notEqual(invalidWorkerPollInterval.status, 0, 'invalid production worker poll interval should fail readiness');
|
|
assert.ok(
|
|
invalidWorkerPollInterval.payload.checks?.some(item => (
|
|
item.id === 'env.worker_crm_poll_interval_ms' && item.status === 'blocker'
|
|
)),
|
|
'readiness should block invalid worker poll intervals',
|
|
);
|
|
|
|
const safeAliyunPnvs = runReadiness(`
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
|
CORS_ORIGIN=https://student.gongxue100.com,https://tenant-admin.gongxue100.com,https://platform-admin.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
|
AUTH_CODE_PEPPER=${strongSecretA}
|
|
AUTH_SESSION_SECRET=${strongSecretB}
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=${strongSecretC}
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
WORKER_CRM_BATCH_SIZE=20
|
|
WORKER_COMMERCE_BATCH_SIZE=20
|
|
WORKER_ASSET_BATCH_SIZE=50
|
|
WORKER_IMPORT_BATCH_SIZE=5
|
|
WORKER_PUBLIC_BANK_SYNC_BATCH_SIZE=5
|
|
`);
|
|
|
|
assert.equal(safeAliyunPnvs.status, 0, `aliyun-pnvs readiness should pass without blockers: ${safeAliyunPnvs.stdout} ${safeAliyunPnvs.stderr}`);
|
|
assert.equal(safeAliyunPnvs.payload.summary?.blocker, 0, 'aliyun-pnvs readiness should have no blockers');
|
|
|
|
const safeAliyunPnvsUnderscoreAlias = runReadiness(`
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
|
CORS_ORIGIN=https://student.gongxue100.com,https://tenant-admin.gongxue100.com,https://platform-admin.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun_pnvs
|
|
AUTH_CODE_PEPPER=${strongSecretA}
|
|
AUTH_SESSION_SECRET=${strongSecretB}
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=${strongSecretC}
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
`, {
|
|
providerRows: [
|
|
{
|
|
source: 'auth',
|
|
tenantId: 'tenant-pnvs',
|
|
provider: 'aliyun-pnvs',
|
|
configPublic: {
|
|
signName: '短信签名',
|
|
templateCode: 'SMS_123456789',
|
|
endpoint: 'https://dypnsapi.aliyuncs.com',
|
|
templateParam: { code: '##code##', min: '5' },
|
|
},
|
|
},
|
|
],
|
|
});
|
|
|
|
assert.equal(safeAliyunPnvsUnderscoreAlias.status, 0, `aliyun_pnvs readiness should pass without blockers: ${safeAliyunPnvsUnderscoreAlias.stdout} ${safeAliyunPnvsUnderscoreAlias.stderr}`);
|
|
assert.equal(safeAliyunPnvsUnderscoreAlias.payload.summary?.blocker, 0, 'aliyun_pnvs readiness should have no blockers');
|
|
assert.ok(
|
|
safeAliyunPnvsUnderscoreAlias.payload.checks?.some(item => item.id === 'db.auth_sms_provider_configured' && item.status === 'pass'),
|
|
'aliyun_pnvs readiness should still validate matching PNVS provider rows',
|
|
);
|
|
|
|
const pnvsTemplateParamWarning = runReadiness(
|
|
`
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
|
CORS_ORIGIN=https://student.gongxue100.com,https://tenant-admin.gongxue100.com,https://platform-admin.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
|
AUTH_CODE_PEPPER=${strongSecretA}
|
|
AUTH_SESSION_SECRET=${strongSecretB}
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=${strongSecretC}
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
`,
|
|
{
|
|
providerRows: [
|
|
{
|
|
source: 'auth',
|
|
tenantId: 'tenant-pnvs',
|
|
provider: 'aliyun-pnvs',
|
|
configPublic: {
|
|
signName: '短信签名',
|
|
templateCode: 'SMS_123456789',
|
|
endpoint: 'https://dypnsapi.aliyuncs.com',
|
|
templateParam: { min: '5' },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
);
|
|
|
|
assert.equal(pnvsTemplateParamWarning.status, 0, 'PNVS templateParam warning should not block readiness');
|
|
assert.ok(
|
|
pnvsTemplateParamWarning.payload.checks?.some(item => item.id === 'db.auth.aliyun-pnvs.template_param' && item.status === 'warn'),
|
|
'readiness should warn when PNVS templateParam lacks ##code##',
|
|
);
|
|
assert.ok(
|
|
pnvsTemplateParamWarning.payload.checks?.some(item => item.id === 'db.auth_sms_provider_configured' && item.status === 'pass'),
|
|
'readiness should pass when AUTH_SMS_PROVIDER has a matching PNVS provider row',
|
|
);
|
|
|
|
const mismatchedSmsProviderFixture = runReadiness(
|
|
`
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
|
CORS_ORIGIN=https://student.gongxue100.com,https://tenant-admin.gongxue100.com,https://platform-admin.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
|
AUTH_CODE_PEPPER=${strongSecretA}
|
|
AUTH_SESSION_SECRET=${strongSecretB}
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=${strongSecretC}
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
`,
|
|
{
|
|
providerRows: [
|
|
{
|
|
source: 'auth',
|
|
tenantId: 'tenant-aliyun',
|
|
provider: 'aliyun',
|
|
configPublic: {
|
|
signName: '短信签名',
|
|
templateCode: 'SMS_123456789',
|
|
endpoint: 'https://dysmsapi.aliyuncs.com',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
);
|
|
|
|
assert.notEqual(mismatchedSmsProviderFixture.status, 0, 'readiness should fail when AUTH_SMS_PROVIDER has no matching active provider row');
|
|
assert.ok(
|
|
mismatchedSmsProviderFixture.payload.checks?.some(item => item.id === 'db.auth_sms_provider_configured' && item.status === 'blocker'),
|
|
'readiness should block env/provider mismatch',
|
|
);
|
|
assert.ok(
|
|
mismatchedSmsProviderFixture.payload.checks?.some(item => item.id === 'db.auth.aliyun.legacy_sms_provider' && item.status === 'blocker'),
|
|
'readiness should block active traditional Aliyun SMS auth provider rows',
|
|
);
|
|
|
|
const legacyTencentSmsProviderFixture = runReadiness(
|
|
`
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
|
CORS_ORIGIN=https://student.gongxue100.com,https://tenant-admin.gongxue100.com,https://platform-admin.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
|
AUTH_CODE_PEPPER=${strongSecretA}
|
|
AUTH_SESSION_SECRET=${strongSecretB}
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=${strongSecretC}
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
`,
|
|
{
|
|
providerRows: [
|
|
{
|
|
source: 'auth',
|
|
tenantId: 'tenant-tencent',
|
|
provider: 'tencent-sms',
|
|
configPublic: {
|
|
smsSdkAppId: '1400000000',
|
|
signName: '短信签名',
|
|
templateId: '123456',
|
|
},
|
|
},
|
|
{
|
|
source: 'auth',
|
|
tenantId: 'tenant-pnvs',
|
|
provider: 'aliyun-pnvs',
|
|
configPublic: {
|
|
signName: '短信签名',
|
|
templateCode: 'SMS_123456789',
|
|
endpoint: 'https://dypnsapi.aliyuncs.com',
|
|
templateParam: { code: '##code##', min: '5' },
|
|
},
|
|
},
|
|
],
|
|
},
|
|
);
|
|
|
|
assert.notEqual(legacyTencentSmsProviderFixture.status, 0, 'readiness should fail when Tencent SMS auth provider rows remain active');
|
|
assert.ok(
|
|
legacyTencentSmsProviderFixture.payload.checks?.some(item => item.id === 'db.auth.tencent-sms.legacy_sms_provider' && item.status === 'blocker'),
|
|
'readiness should block active Tencent SMS auth provider rows',
|
|
);
|
|
|
|
const unsafeProviderFixture = runReadiness(
|
|
`
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
|
CORS_ORIGIN=https://student.gongxue100.com,https://tenant-admin.gongxue100.com,https://platform-admin.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
|
AUTH_CODE_PEPPER=${strongSecretA}
|
|
AUTH_SESSION_SECRET=${strongSecretB}
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=${strongSecretC}
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
`,
|
|
{
|
|
providerRows: [
|
|
{
|
|
source: 'auth',
|
|
tenantId: 'tenant-a',
|
|
provider: 'qq-oauth',
|
|
configPublic: {
|
|
appId: '101000000',
|
|
redirectUri: 'http://localhost:5173/auth/qq/callback',
|
|
},
|
|
},
|
|
{
|
|
source: 'payment',
|
|
tenantId: 'tenant-a',
|
|
provider: 'wechat_pay',
|
|
configPublic: {
|
|
appId: 'wx123',
|
|
merchantId: '1900000001',
|
|
merchantSerialNo: 'serial123',
|
|
},
|
|
},
|
|
{
|
|
source: 'payment',
|
|
tenantId: 'tenant-b',
|
|
provider: 'alipay',
|
|
configPublic: {
|
|
appId: '2021000000000000',
|
|
notifyUrl: 'http://pay.example.com/notify',
|
|
privateKey: 'should-not-be-public',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
);
|
|
|
|
assert.notEqual(unsafeProviderFixture.status, 0, 'unsafe provider fixture readiness should fail');
|
|
assert.ok(
|
|
unsafeProviderFixture.payload.checks?.some(item => item.id === 'db.auth.qq-oauth.redirect_uri.unsafe' && item.status === 'blocker'),
|
|
'readiness should block unsafe QQ redirectUri',
|
|
);
|
|
assert.ok(
|
|
unsafeProviderFixture.payload.checks?.some(item => item.id === 'db.payment.wechat_pay.public_required' && item.status === 'blocker'),
|
|
'readiness should block missing WeChat Pay notifyUrl',
|
|
);
|
|
assert.ok(
|
|
unsafeProviderFixture.payload.checks?.some(item => item.id === 'db.payment.alipay.notify_url.unsafe' && item.status === 'blocker'),
|
|
'readiness should block unsafe Alipay notifyUrl',
|
|
);
|
|
assert.ok(
|
|
unsafeProviderFixture.payload.checks?.some(item => item.id === 'db.payment.alipay.public_secret' && item.status === 'blocker'),
|
|
'readiness should block secret-like public payment config keys',
|
|
);
|
|
|
|
const missingJwksIssuer = runReadiness(`
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
|
CORS_ORIGIN=https://student.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
|
AUTH_CODE_PEPPER=${strongSecretA}
|
|
AUTH_SESSION_SECRET=${strongSecretB}
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=${strongSecretC}
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
WORKER_PLATFORM_AUDIT_NOTIFICATION_ALLOW_INSECURE_LOCALHOST=false
|
|
WORKER_PLATFORM_DUNNING_NOTIFICATION_ALLOW_INSECURE_LOCALHOST=false
|
|
`);
|
|
|
|
assert.notEqual(missingJwksIssuer.status, 0, 'JWKS readiness without issuer should fail');
|
|
assert.ok(
|
|
missingJwksIssuer.payload.checks?.some(item => item.id === 'env.auth_jwt_issuer' && item.status === 'blocker'),
|
|
'JWKS readiness should block missing AUTH_JWT_ISSUER',
|
|
);
|
|
|
|
const unsafePlatformAuditNotificationLocalhost = runReadiness(`
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
|
CORS_ORIGIN=https://student.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
|
AUTH_CODE_PEPPER=${strongSecretA}
|
|
AUTH_SESSION_SECRET=${strongSecretB}
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=${strongSecretC}
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
WORKER_PLATFORM_AUDIT_NOTIFICATION_ALLOW_INSECURE_LOCALHOST=true
|
|
`);
|
|
|
|
assert.notEqual(unsafePlatformAuditNotificationLocalhost.status, 0, 'platform audit notification localhost readiness should fail');
|
|
assert.ok(
|
|
unsafePlatformAuditNotificationLocalhost.payload.checks?.some(item => item.id === 'env.worker_platform_audit_notification_insecure_localhost' && item.status === 'blocker'),
|
|
'readiness should block platform audit notification localhost mode in production',
|
|
);
|
|
|
|
const unsafePlatformDunningNotificationLocalhost = runReadiness(`
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
|
CORS_ORIGIN=https://student.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
|
AUTH_CODE_PEPPER=${strongSecretA}
|
|
AUTH_SESSION_SECRET=${strongSecretB}
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=${strongSecretC}
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
WORKER_PLATFORM_AUDIT_NOTIFICATION_ALLOW_INSECURE_LOCALHOST=false
|
|
WORKER_PLATFORM_DUNNING_NOTIFICATION_ALLOW_INSECURE_LOCALHOST=true
|
|
`);
|
|
|
|
assert.notEqual(unsafePlatformDunningNotificationLocalhost.status, 0, 'platform dunning notification localhost readiness should fail');
|
|
assert.ok(
|
|
unsafePlatformDunningNotificationLocalhost.payload.checks?.some(item => item.id === 'env.worker_platform_dunning_notification_insecure_localhost' && item.status === 'blocker'),
|
|
'readiness should block platform dunning notification localhost mode in production',
|
|
);
|
|
|
|
const tenantConfigBaseEnv = `
|
|
NODE_ENV=production
|
|
DATABASE_URL=postgresql://prod_user:prod_password@db.prod.internal:5432/tiku
|
|
CORS_ORIGIN=https://student.gongxue100.com
|
|
CORS_TENANT_DOMAINS_ENABLED=true
|
|
CORS_TENANT_DOMAIN_CACHE_TTL_MS=60000
|
|
CORS_TENANT_DOMAIN_NEGATIVE_CACHE_TTL_MS=10000
|
|
CORS_TENANT_DOMAIN_CACHE_MAX_ENTRIES=10000
|
|
AUTH_SMS_PROVIDER=aliyun-pnvs
|
|
AUTH_CODE_PEPPER=${strongSecretA}
|
|
AUTH_SESSION_SECRET=${strongSecretB}
|
|
AUTH_JWT_JWKS_URL=https://auth.gongxue100.com/auth/v1/.well-known/jwks.json
|
|
AUTH_JWT_ISSUER=https://auth.gongxue100.com/auth/v1
|
|
ALLOW_LEGACY_AUTH_HEADERS=false
|
|
ALLOW_PLATFORM_ADMIN_KEY=false
|
|
PLATFORM_ADMIN_API_KEY=${strongSecretC}
|
|
STORAGE_DEFAULT_PROVIDER=aliyun_oss
|
|
STORAGE_DEFAULT_BUCKET=tiku-assets
|
|
STORAGE_REQUIRE_TENANT_PREFIX=true
|
|
ALIYUN_OSS_REGION=cn-hangzhou
|
|
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
|
ALIYUN_OSS_ACCESS_KEY_ID=LTAI_READINESS_TEST_ONLY
|
|
ALIYUN_OSS_ACCESS_KEY_SECRET=aliyun-readiness-secret-placeholder
|
|
WORKER_ASSET_SECURITY_SCANNER=metadata_rules,http
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_ENDPOINT=https://scanner.gongxue100.com/api/scan
|
|
WORKER_ASSET_SECURITY_SCAN_HTTP_TOKEN=s3cure-asset-scanner-token-2026-06-29-stuvwx
|
|
WORKER_ASSET_SECURITY_SCAN_FAIL_OPEN=false
|
|
`;
|
|
|
|
const unsafeTenantPublicUrl = runReadiness(tenantConfigBaseEnv, {
|
|
tenantRows: [{
|
|
tenantId: 'tenant-local-url',
|
|
slug: 'local-url',
|
|
name: 'Local URL tenant',
|
|
publicConfig: { frontend: { appUrl: 'http://127.0.0.1:5173' } },
|
|
publishedTheme: { primaryColor: '#2563eb' },
|
|
themeStatus: 'published',
|
|
publishedAt: '2026-07-11T00:00:00.000Z',
|
|
}],
|
|
});
|
|
|
|
assert.notEqual(unsafeTenantPublicUrl.status, 0, 'active tenant localhost public URLs should fail readiness');
|
|
assert.ok(
|
|
unsafeTenantPublicUrl.payload.checks?.some(item => item.id === 'db.tenant_public_urls' && item.status === 'blocker'),
|
|
'readiness should block active tenant public URLs that are not production HTTPS',
|
|
);
|
|
|
|
const tenantWithoutPublishedTheme = runReadiness(tenantConfigBaseEnv, {
|
|
tenantRows: [{
|
|
tenantId: 'tenant-default-theme',
|
|
slug: 'default-theme',
|
|
name: 'Default theme tenant',
|
|
publicConfig: { appUrl: 'https://student.gongxue100.com' },
|
|
}],
|
|
});
|
|
|
|
assert.equal(tenantWithoutPublishedTheme.status, 0, 'missing tenant theme should use platform defaults without blocking readiness');
|
|
assert.ok(
|
|
tenantWithoutPublishedTheme.payload.checks?.some(item => item.id === 'db.tenant_theme_published' && item.status === 'warn'),
|
|
'readiness should warn when an active tenant has no published or branding fallback theme',
|
|
);
|
|
|
|
const tenantWithBrandingFallback = runReadiness(tenantConfigBaseEnv, {
|
|
tenantRows: [{
|
|
tenantId: 'tenant-branding-theme',
|
|
slug: 'branding-theme',
|
|
name: 'Branding theme tenant',
|
|
publicConfig: { appUrl: 'https://student.gongxue100.com' },
|
|
brandingTheme: { primaryColor: '#0f766e' },
|
|
}],
|
|
});
|
|
|
|
assert.equal(tenantWithBrandingFallback.status, 0, 'branding theme fallback should pass readiness');
|
|
assert.ok(
|
|
tenantWithBrandingFallback.payload.checks?.some(item => item.id === 'db.tenant_theme_published' && item.status === 'pass'),
|
|
'readiness should accept a non-empty tenant branding fallback theme',
|
|
);
|
|
|
|
for (const environmentSafetyRow of [
|
|
null,
|
|
{ environment: 'production', allowDestructiveTests: false },
|
|
{ environment: 'staging', allow_destructive_tests: false },
|
|
]) {
|
|
const result = runReadiness(tenantConfigBaseEnv, { environmentSafetyRow });
|
|
assert.equal(
|
|
result.status,
|
|
0,
|
|
`production readiness should accept a missing or disabled production/staging marker: ${result.stdout} ${result.stderr}`,
|
|
);
|
|
assert.ok(
|
|
result.payload.checks?.some(item => (
|
|
item.id === 'db.environment.destructive_tests_disabled' && item.status === 'pass'
|
|
)),
|
|
'production readiness should record the safe destructive-test marker state',
|
|
);
|
|
}
|
|
|
|
for (const environmentSafetyRow of [
|
|
{ environment: 'local', allowDestructiveTests: false },
|
|
{ environment: 'test', allowDestructiveTests: false },
|
|
{ environment: 'ci', allowDestructiveTests: false },
|
|
{ environment: 'production', allowDestructiveTests: true },
|
|
{ environment: 'staging', allowDestructiveTests: true },
|
|
{ environment: 'unknown', allowDestructiveTests: false },
|
|
{ environment: 'production', allowDestructiveTests: 'false' },
|
|
]) {
|
|
const result = runReadiness(tenantConfigBaseEnv, { environmentSafetyRow });
|
|
assert.notEqual(
|
|
result.status,
|
|
0,
|
|
`production readiness must reject unsafe destructive-test marker ${JSON.stringify(environmentSafetyRow)}`,
|
|
);
|
|
assert.ok(
|
|
result.payload.checks?.some(item => (
|
|
item.id === 'db.environment.destructive_tests_disabled' && item.status === 'blocker'
|
|
)),
|
|
'production readiness should block unsafe destructive-test marker state',
|
|
);
|
|
}
|
|
|
|
const readinessSource = fs.readFileSync(scriptPath, 'utf8');
|
|
assert.match(
|
|
readinessSource,
|
|
/to_regclass\('app_private\.environment_safety'\)/,
|
|
'database readiness must verify that the environment safety migration exists',
|
|
);
|
|
assert.match(
|
|
readinessSource,
|
|
/from app_private\.environment_safety[\s\S]*where id = true/i,
|
|
'database readiness must read the authoritative destructive-test marker',
|
|
);
|
|
for (const gateId of [
|
|
'db.runtime_role.identity',
|
|
'db.runtime_role.attributes',
|
|
'db.runtime_role.schema_acl',
|
|
'db.runtime_role.table_acl',
|
|
'db.runtime_role.function_acl',
|
|
'db.runtime_role.ownership',
|
|
'db.runtime_role.ddl_denied',
|
|
]) {
|
|
assert.ok(readinessSource.includes(gateId), `database readiness must include ${gateId}`);
|
|
}
|
|
assert.match(
|
|
readinessSource,
|
|
/current_user[\s\S]*session_user[\s\S]*DB_EXPECTED_RUNTIME_ROLE/i,
|
|
'database readiness must verify current_user and session_user against DB_EXPECTED_RUNTIME_ROLE',
|
|
);
|
|
assert.match(
|
|
readinessSource,
|
|
/has_database_privilege[\s\S]*has_schema_privilege[\s\S]*db\.runtime_role\.ddl_denied/i,
|
|
'database readiness must verify effective persistent DDL privileges without writing to production',
|
|
);
|
|
|
|
console.log('[PASS] production readiness check script');
|