const { spawnSync, execSync } = require('child_process'); const { Before, After, Given, When, Then } = require('@cucumber/cucumber'); const path = require('path'); const PROJECT_ROOT = path.resolve(__dirname, '..', '..', '..'); const MOCK_SCRIPT = path.join(PROJECT_ROOT, 'tests', 'helpers', 'mock-api.sh'); const GITOPS_SCRIPT = path.join(PROJECT_ROOT, 'scripts', 'gitops-update.sh'); const MOCK_HELPERS = path.join(PROJECT_ROOT, 'tests', 'helpers'); const REQ_FILE = '/tmp/gitops-mock-requests.log'; const BASE_ENV = { INPUT_FILE: 'dev/Chart.yaml', YQ_TPL: '(.version) = "{{VERSION}}"', VERSION: '0.2.3', SOURCE_REPO: 'niko/app', SOURCE_COMMIT: 'abc123def456', GITOPS_REPO: 'niko/app-gitops', GITEA_API_URL: 'http://localhost:18080', GITEA_TOKEN: 'test-token', }; Before({ tags: '@mock' }, function () { process.env.PATH = `${MOCK_HELPERS}:${process.env.PATH}`; try { execSync('rm -f /tmp/gitops-mock-requests.log', { stdio: 'ignore' }); } catch (_) {} // Restart mock with known request file path const result = spawnSync('bash', ['-c', ` source "${MOCK_SCRIPT}" mock_stop 2>/dev/null MOCK_REQUEST_FILE="${REQ_FILE}" mock_start sleep 0.5 curl -s -o /dev/null -w "%{http_code}" --max-time 3 http://localhost:18080/api/v1/repos/health `], { cwd: PROJECT_ROOT, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }); const code = result.stdout.trim(); if (!code.startsWith('2') && !code.startsWith('4')) { throw new Error(`GitOps mock restart failed: ${result.stderr.substring(0,200)}`); } }); After({ tags: '@mock' }, function () { spawnSync('bash', ['-c', `source "${MOCK_SCRIPT}" && mock_stop 2>/dev/null`], { stdio: 'ignore' }); try { execSync('rm -f /tmp/gitops-mock-requests.log /tmp/gitops-git-calls.log', { stdio: 'ignore' }); } catch (_) {} }); function bash(cmd) { const result = spawnSync('bash', ['-c', cmd], { cwd: PROJECT_ROOT, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'], }); return { status: result.status, stdout: result.stdout || '', stderr: result.stderr || '' }; } function getFirstBody() { return bash(`grep -A1 '^POST ' "${REQ_FILE}" 2>/dev/null | head -2 | tail -1 || echo ""`).stdout.trim(); } function getFirstPath() { return bash(`grep '^POST ' "${REQ_FILE}" 2>/dev/null | head -1 | awk '{print $2}' || echo ""`).stdout.trim(); } function getLastBody() { return bash(`grep -A1 '^POST ' "${REQ_FILE}" 2>/dev/null | grep -v '^POST ' | tail -1 || echo ""`).stdout.trim(); } function getLastPath() { return bash(`grep '^POST ' "${REQ_FILE}" 2>/dev/null | tail -1 | awk '{print $2}' || echo ""`).stdout.trim(); } function requestCount() { return parseInt(bash(`grep -c '^POST ' "${REQ_FILE}" 2>/dev/null || echo 0`).stdout.trim(), 10) || 0; } function gitCalls() { const callsFile = process.env.GIT_CALLS_FILE || '/dev/null'; const out = bash(`cat "${callsFile}" 2>/dev/null || echo ""`).stdout; return out.split('\n').filter(l => l.length > 0); } function runScript(envOverrides) { const env = { ...BASE_ENV, ...envOverrides }; const scriptPath = `/tmp/gitops-run-${Date.now()}.sh`; const exports = Object.entries(env) .map(([k, v]) => `export ${k}="${v.replace(/"/g, '\\"')}"`) .join('\n'); require('fs').writeFileSync(scriptPath, `${exports}\nexport PATH="${MOCK_HELPERS}:$PATH"\nset -euo pipefail\nbash "${GITOPS_SCRIPT}"\nsync\n`, 'utf8'); try { return bash(`bash "${scriptPath}"`); } finally { require('fs').unlinkSync(scriptPath); } } Given('insufficient environment variables are provided for the GitOps update', function () { this.envOverrides = { INPUT_FILE: '' }; }); Given('the GitOps repository clone will fail', function () { this.envOverrides = { GIT_MOCK_FAIL: '1' }; }); Given('a valid GitOps update dispatch', function () { this.envOverrides = {}; }); Given('the GitOps repo push will fail after the version is committed', function () { this.envOverrides = { GIT_MOCK_FAIL_PUSH: '1' }; }); Given('the version file already has the target version', function () { this.envOverrides = { GIT_MOCK_DIFF_NO_CHANGES: '1', GIT_CALLS_FILE: '/tmp/gitops-git-calls.log', }; }); When('the GitOps update script runs', function () { this.result = runScript(this.envOverrides || {}); }); Then('the script exits with error', function () { if (this.result.status === 0) throw new Error(`Expected non-zero exit, got 0. stderr: ${this.result.stderr.substring(0,200)}`); }); Then('the script exits successfully', function () { if (this.result.status !== 0) throw new Error(`Expected exit 0, got ${this.result.status}: ${this.result.stderr.substring(0,200)}`); }); Then('the GitOps repo commit shows a success status with a link to the caller commit', function () { const count = requestCount(); if (count < 1) throw new Error(`Expected at least 1 request, got ${count}`); const body = getFirstBody(); if (!body.includes('"state":"success"')) throw new Error(`Expected success state, body: ${body.substring(0,200)}`); if (!body.includes('"context":"app ')) throw new Error(`Expected context "app unknown", body: ${body.substring(0,200)}`); if (!body.includes('"description":"Install to dev 0.2.3"')) throw new Error(`Expected description, body: ${body.substring(0,200)}`); if (!body.includes('niko/app/commit/abc123def456')) throw new Error(`Expected link to caller commit, body: ${body.substring(0,200)}`); const pathStr = getFirstPath(); if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error(`Expected gitops repo path, got: ${pathStr}`); }); Then('the GitOps repo commit shows a failure status linking to the caller commit', function () { const count = requestCount(); if (count < 1) throw new Error(`Expected at least 1 request, got ${count}`); const body = getFirstBody(); if (!body.includes('"state":"failure"')) throw new Error(`Expected failure state, body: ${body.substring(0,200)}`); if (!body.includes('"context":"app ')) throw new Error(`Expected context "app unknown", body: ${body.substring(0,200)}`); if (!body.includes('"description":"Install to dev 0.2.3"')) throw new Error(`Expected description, body: ${body.substring(0,200)}`); if (!body.includes('niko/app/commit/abc123def456')) throw new Error(`Expected link to caller commit, body: ${body.substring(0,200)}`); const pathStr = getFirstPath(); if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error(`Expected gitops repo path, got: ${pathStr}`); }); Then('the GitOps repo commit shows a "no change" status', function () { const count = requestCount(); if (count < 1) throw new Error(`Expected at least 1 request, got ${count}`); const body = getFirstBody(); if (!body.includes('"state":"success"')) throw new Error(`Expected success state, body: ${body.substring(0,200)}`); if (!body.includes('"description":"Install to dev 0.2.3 \u2014 no change"')) { throw new Error(`Expected "no change" description, body: ${body.substring(0,200)}`); } const pathStr = getFirstPath(); if (!pathStr.includes('/repos/niko/app-gitops/statuses/')) throw new Error(`Expected gitops repo path, got: ${pathStr}`); }); Then('no Git commit or push was performed', function () { const calls = gitCalls(); if (calls.some(l => l.includes(' commit ') || l.includes(' push '))) { throw new Error(`Expected no commit or push, got: ${calls.join(', ')}`); } });