import assert from 'node:assert/strict'; import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { spawnSync } from 'node:child_process'; import { test } from 'node:test'; import { fileURLToPath } from 'node:url'; const scriptPath = fileURLToPath(new URL('../scripts/deploy-remote.sh', import.meta.url)); const canRun = process.platform !== 'win32'; test('waits for app and postfix health before and after certificate synchronization', { skip: !canRun }, (t) => { const fixture = createFixture(t); const result = runDeploy(fixture); assert.equal(result.status, 0, result.stderr); const events = readEvents(fixture.logFile); assert.deepEqual( events.filter((event) => event.startsWith('health:') || event.startsWith('sync:')), [ 'health:app-container', 'health:postfix-container', 'sync:1', 'health:app-container', 'health:postfix-container' ] ); }); test('reports the previous revision when deployment fails', { skip: !canRun }, (t) => { const fixture = createFixture(t); const result = runDeploy(fixture, { syncStatus: '19' }); assert.equal(result.status, 19); assert.match( result.stderr, /Deployment failed\. Previous revision was previous-revision; inspect the running containers before rollback\./ ); const events = readEvents(fixture.logFile); assert.equal(events.filter((event) => event === 'sync:1').length, 1); assert.equal(events.filter((event) => event === 'health:app-container').length, 1); assert.equal(events.filter((event) => event === 'health:postfix-container').length, 1); }); function createFixture(t) { const root = mkdtempSync(path.join(os.tmpdir(), 'mailhub-deploy-script-')); const fakeBin = path.join(root, 'bin'); const remoteDir = path.join(root, 'remote'); const remoteScriptsDir = path.join(remoteDir, 'scripts'); const logFile = path.join(root, 'events.log'); mkdirSync(fakeBin, { recursive: true }); mkdirSync(remoteScriptsDir, { recursive: true }); writeFileSync(logFile, ''); writeExecutable(path.join(fakeBin, 'git'), `#!/bin/sh if [ "$1" = "remote" ] && [ "$2" = "get-url" ]; then printf '%s\\n' 'ssh://git.example.test/mailhub.git' elif [ "$1" = "status" ]; then : elif [ "$1" = "rev-parse" ] && [ "$2" = "HEAD" ]; then if [ "$PWD" = "$MAILHUB_DEPLOY_TEST_REMOTE_DIR" ]; then printf '%s\\n' 'previous-revision' else printf '%s\\n' 'pushed-revision' fi elif [ "$1" = "rev-parse" ]; then printf '%s\\n' 'pushed-revision' fi `); writeExecutable(path.join(fakeBin, 'ssh'), `#!/bin/sh while [ "$#" -gt 0 ]; do case "$1" in -o) shift 2 ;; *) break ;; esac done [ "$#" -gt 0 ] && shift [ "$#" -gt 0 ] && shift [ "\${1:-}" = "--" ] && shift exec bash -s -- "$@" `); writeExecutable(path.join(fakeBin, 'docker'), `#!/bin/sh if [ "$1" = "compose" ] && [ "$2" = "ps" ] && [ "\${3:-}" = "--all" ]; then printf '%s-container\\n' "$5" exit 0 fi if [ "$1" = "inspect" ]; then container='' for argument in "$@"; do container="$argument" done printf 'health:%s\\n' "$container" >> "$MAILHUB_DEPLOY_TEST_LOG" printf '%s\\n' 'running healthy' fi `); writeExecutable(path.join(remoteScriptsDir, 'sync-tls-certificate.sh'), `#!/bin/sh printf 'sync:%s\\n' "\${MAILHUB_CERT_RESTART:-}" >> "$MAILHUB_DEPLOY_TEST_LOG" exit "\${MAILHUB_DEPLOY_TEST_SYNC_STATUS:-0}" `); t.after(() => rmSync(root, { recursive: true, force: true })); return { root, fakeBin, remoteDir, logFile }; } function runDeploy(fixture, { syncStatus = '0' } = {}) { return spawnSync('bash', [scriptPath], { cwd: fixture.root, encoding: 'utf8', env: { ...process.env, PATH: `${fixture.fakeBin}${path.delimiter}${process.env.PATH ?? ''}`, MAILHUB_DEPLOY_REMOTE: 'deploy@example.test', MAILHUB_DEPLOY_DIR: fixture.remoteDir, MAILHUB_DEPLOY_BRANCH: 'main', MAILHUB_DEPLOY_GIT_URL: 'ssh://git.example.test/mailhub.git', MAILHUB_DEPLOY_TEST_LOG: fixture.logFile, MAILHUB_DEPLOY_TEST_REMOTE_DIR: fixture.remoteDir, MAILHUB_DEPLOY_TEST_SYNC_STATUS: syncStatus } }); } function writeExecutable(filePath, contents) { writeFileSync(filePath, contents); chmodSync(filePath, 0o755); } function readEvents(logFile) { return readFileSync(logFile, 'utf8').trim().split('\n').filter(Boolean); }