import fs from 'fs' import path from 'path' describe('deploy-registry.ps1', () => { const script = fs.readFileSync( path.join(process.cwd(), 'deploy-registry.ps1'), 'utf8' ) it('远程部署默认包含升级前数据库、对象存储和 .env 备份', () => { expect(script).toContain('[switch]$NoRemoteDbBackup') expect(script).toContain('[switch]$NoRemoteObjectStorageBackup') expect(script).toContain('[switch]$ExternalDataBackupConfirmed') expect(script).toContain('[string]$RemoteBackupHelperImage = "busybox:1.36"') expect(script).toContain('env_backup="$backup_dir/.env.before-upgrade"') expect(script).toContain('enter_maintenance_for_backup') expect(script).toContain('APP_STOPPED_FOR_BACKUP.txt') expect(script).toContain("docker compose -f \"$compose_file\" exec -T postgres sh -c 'pg_dump") expect(script).toContain('test -s "$db_backup"') expect(script).toContain('backup_minio_volume()') expect(script).toContain('docker volume inspect "$minio_volume_name"') expect(script).toContain('tar -czf /backup/minio-data.tar.gz') expect(script).toContain('rm -rf /target/* /target/.[!.]* /target/..?*') expect(script).toContain('OBJECT_STORAGE_EXTERNAL.txt') expect(script).toContain('backup_minio_volume') expect(script).toContain('is_external_database()') expect(script).toContain('is_external_object_storage()') expect(script).toContain('EXTERNAL_DATABASE_BACKUP_REQUIRED.txt') expect(script).toContain('EXTERNAL_OBJECT_STORAGE_BACKUP_REQUIRED.txt') expect(script).toContain('external_data_backup_confirmed=$externalDataBackupConfirmed') expect(script).toContain('外部数据库备份未确认,已中止升级') expect(script).toContain('外部对象存储备份未确认,已中止升级') }) it('远程部署失败时默认会恢复旧 APP_IMAGE 或旧容器镜像', () => { expect(script).toContain('[switch]$NoRollback') expect(script).toContain('rollback()') expect(script).toContain('trap rollback ERR') expect(script.indexOf('trap rollback ERR')).toBeLessThan(script.indexOf('upsert_env APP_IMAGE')) expect(script).toContain('write_deploy_context') expect(script).toContain('LAST_DEPLOY_CONTEXT.env') expect(script).toContain('rollback_image="$old_container_image"') expect(script).toContain('docker compose -f "$compose_file" up -d --no-deps --pull never --force-recreate "$compose_service" || true') expect(script).toContain('自动回滚只恢复应用镜像和 .env,不自动恢复数据库') }) it('本地最终健康检查失败时会触发远程回滚', () => { expect(script).toContain('function New-RemoteRollbackScript') expect(script).toContain('应用健康检查失败,尝试远程回滚') expect(script).toContain('New-RemoteRollbackScript -Reason $healthError') expect(script).toContain('Invoke-RemoteCommand -Command $rollbackScript -NoRegistryPasswordInput') expect(script).toContain('本地最终健康检查失败,准备远程回滚') expect(script).toContain('cp "$env_backup" .env') expect(script).toContain('docker compose -f "`${compose_file:-docker-compose.prod.yml}" up -d --no-deps --pull never --force-recreate "`${compose_service:-app}"') }) it('部署后校验运行镜像、版本文件和数据库迁移状态', () => { expect(script).toContain('verify_running_image') expect(script).toContain('actual_image="$(docker inspect "$container_id" --format') expect(script).toContain('运行镜像不匹配') expect(script).toContain('运行版本不匹配') expect(script).toContain('npx prisma migrate status --schema=prisma/schema.prisma') }) it('远程健康检查使用可配置超时和最终访问入口', () => { expect(script).toContain('[int]$RemoteHealthTimeoutSec = 120') expect(script).toContain('$baseUrl = if (-not [string]::IsNullOrWhiteSpace($AppPublicUrl))') expect(script).toContain('$deadline = (Get-Date).AddSeconds($RemoteHealthTimeoutSec)') expect(script).toContain('健康检查没有在 $RemoteHealthTimeoutSec 秒内通过') expect(script).not.toContain('健康检查没有在 60 秒内通过') }) it('运行时镜像同步包含对象存储备份 helper', () => { expect(script).toContain('Source = "busybox:1.36"') expect(script).toContain('Repository = "runtime/busybox"') expect(script).toContain('Tag = "1.36"') }) it('支持发布付费模块镜像并输出验证服务器 digest 配置', () => { expect(script).toContain('"PublishPremiumModule"') expect(script).toContain('[string]$PremiumModuleId = "intranet-penetration-core"') expect(script).toContain('[string]$PremiumModuleRepositoryPrefix = "premium"') expect(script).toContain('[switch]$AllowMissingPremiumModuleDigest') expect(script).toContain('function Invoke-PublishPremiumModule') expect(script).toContain('$moduleRepository = "$($PremiumModuleRepositoryPrefix.Trim(\'/\'))/$moduleId"') expect(script).toContain('if (-not $SkipPush)') expect(script).toContain('未能自动读取付费模块镜像 digest') expect(script).toContain('Get-PushedImageDigest -Image $registryImage') expect(script).toContain('PREMIUM_MODULE_${envKey}_DIGEST=$digest') }) it('基础镜像构建上下文必须排除付费模块源码', () => { const dockerignore = fs.readFileSync(path.join(process.cwd(), '.dockerignore'), 'utf8') expect(dockerignore).toContain('premium-modules') expect(dockerignore).toContain('deploy/registry-proxy') }) it('试跑和只构建付费模块时不应强制连接 Registry', () => { expect(script).toContain('if ($DryRun) {\n return\n }') expect(script).toContain('已跳过推送付费模块镜像;未推送时无法作为客户拉取源使用') expect(script).toContain('$digest = if ($SkipPush) { "" } else { Get-PushedImageDigest -Image $registryImage }') }) it('远端 shell 变量应以单引号字符串保留,避免被 PowerShell 提前展开', () => { expect(script).toContain('$lines.Add(\'docker compose -f "$compose_file" up -d --pull never\')') expect(script).toContain('$lines.Add(\'docker compose -f "$compose_file" ps\')') expect(script).not.toContain('$lines.Add("docker compose -f `"$compose_file`"') }) it('支持飞牛/NAS 普通管理员通过 sudo 执行远程 Docker 升级', () => { expect(script).toContain('[switch]$RemoteSudo') expect(script).toContain('[SecureString]$RemoteSudoPassword = $null') expect(script).toContain('function Ensure-RemoteSudoPasswordIfNeeded') expect(script).toContain('$script:RemoteSudoPassword = $Password') expect(script).toContain('remote_sudo_enabled=$remoteSudoEnabled') expect(script).toContain('IFS= read -r SUDO_PASSWORD') expect(script).toContain('sudo -n docker "$@"') expect(script).toContain('sudo -n docker "`$@"') expect(script).toContain('Invoke-RemoteCommand -Command $rollbackScript -NoRegistryPasswordInput') }) it('发送到 Linux 的远程脚本会统一去除 Windows 回车符', () => { expect(script).toContain('function Normalize-RemoteShellScript') expect(script).toContain('($Script -replace "`r`n", "`n") -replace "`r", ""') expect(script).toContain('$normalizedCommand = Normalize-RemoteShellScript -Script $Command') expect(script).toContain('ConvertTo-ShellSingleQuoted -Value $normalizedCommand') }) it('读取远端 .env 时会去除 Windows 行尾回车', () => { expect(script).toContain('gsub(/\\r$/, "", value)') }) })