Update
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createRequire } from 'node:module';
|
||||
const require = createRequire(import.meta.url);
|
||||
const { UnraidDeploymentService, safeRemoteFolder, safeRelativeRemoteFile, parseInspection, dockerIgnoreHasPath, checksSummary, bash } = require('../src/main/unraid-deployment-service.cjs');
|
||||
const { fingerprintKey, shellQuote } = require('../src/main/ssh-service.cjs');
|
||||
|
||||
test('Unraid remote paths cannot escape appdata project folder', () => {
|
||||
assert.equal(safeRemoteFolder('lumaops'), 'lumaops');
|
||||
assert.throws(() => safeRemoteFolder('../lumaops'));
|
||||
assert.equal(safeRelativeRemoteFile('deploy/docker-compose.yml'), 'deploy/docker-compose.yml');
|
||||
assert.throws(() => safeRelativeRemoteFile('../../etc/passwd'));
|
||||
});
|
||||
|
||||
test('server inspection key-value payload is decoded safely', () => {
|
||||
const b64 = (value) => Buffer.from(value).toString('base64');
|
||||
const parsed = parseInspection(`noise\n__FORGEFLOW_KV__\nexists=true\nrootGit=true\nhead=${'a'.repeat(40)}\nbranch=main\nremote=${b64('ssh://git@gitea/Jens/LumaOps.git')}\ntrackedChanges=${b64(' M docker-compose.yml\n')}\ncomposeFiles=${b64('docker-compose.yml\n')}\nnestedGit=${b64('source\n')}\ndockerfile=true\ndockerignoreContent=${b64('.git\ndata/\n')}\nexistingPreservePaths=${b64('data\nlogs\n')}\n`);
|
||||
assert.equal(parsed.rootGit, true);
|
||||
assert.deepEqual(parsed.composeFiles, ['docker-compose.yml']);
|
||||
assert.deepEqual(parsed.nestedGit, ['source']);
|
||||
assert.equal(parsed.trackedChanges.length, 1);
|
||||
assert.match(parsed.dockerignoreContent, /\.git/);
|
||||
assert.deepEqual(parsed.existingPreservePaths, ['data', 'logs']);
|
||||
});
|
||||
|
||||
test('Docker ignore checks identify exact runtime and Git context exclusions', () => {
|
||||
const rules = '# build context\n.git\ndata/\nlogs/**\n!logs/keep.txt\n';
|
||||
assert.equal(dockerIgnoreHasPath(rules, '.git'), true);
|
||||
assert.equal(dockerIgnoreHasPath(rules, 'data'), true);
|
||||
assert.equal(dockerIgnoreHasPath(rules, 'logs'), true);
|
||||
assert.equal(dockerIgnoreHasPath(rules, 'source'), false);
|
||||
});
|
||||
|
||||
test('server inspection detects preserved runtime paths and missing Docker context exclusions', async () => {
|
||||
const b64 = (value) => Buffer.from(value).toString('base64');
|
||||
let receivedCommand = '';
|
||||
const store = {
|
||||
getDeploymentProfile: () => ({
|
||||
id: 'production',
|
||||
provider: 'ssh-unraid',
|
||||
serverId: 'unraid',
|
||||
remoteFolder: 'lumaops',
|
||||
preservePaths: ['data', 'logs']
|
||||
}),
|
||||
getServer: () => ({
|
||||
id: 'unraid',
|
||||
name: 'Unraid',
|
||||
basePath: '/mnt/user/appdata'
|
||||
})
|
||||
};
|
||||
const ssh = {
|
||||
exec: async (_serverId, command) => {
|
||||
receivedCommand = command;
|
||||
return {
|
||||
stdout: `__FORGEFLOW_KV__\nexists=true\nrootGit=true\nhead=${'a'.repeat(40)}\nbranch=main\nremote=${b64('ssh://git@gitea/Jens/LumaOps.git')}\ntrackedChanges=\ncomposeFiles=${b64('docker-compose.yml\n')}\nnestedGit=${b64('source\n')}\ndockerfile=true\ndockerignoreContent=${b64('.git\ndata/\n')}\nexistingPreservePaths=${b64('data\nlogs\n')}\n`,
|
||||
stderr: '',
|
||||
exitCode: 0
|
||||
};
|
||||
}
|
||||
};
|
||||
const service = new UnraidDeploymentService({ store, ssh, git: {}, diagnostics: null });
|
||||
const inspection = await service.inspect({ repository: { fullName: 'Jens/LumaOps', name: 'LumaOps' }, profileId: 'production' });
|
||||
|
||||
assert.match(receivedCommand, /base64 -d \| bash$/);
|
||||
assert.equal(inspection.remotePath, '/mnt/user/appdata/lumaops');
|
||||
assert.equal(inspection.dockerignoreGitExcluded, true);
|
||||
assert.deepEqual(inspection.existingPreservePaths.sort(), ['data', 'logs']);
|
||||
assert.deepEqual(inspection.dockerContextExclusionsMissing.sort(), ['logs', 'source']);
|
||||
});
|
||||
|
||||
test('preflight summary blocks only failed checks', () => {
|
||||
const result = checksSummary([{ id: 'a', status: 'pass' }, { id: 'b', status: 'warning' }, { id: 'c', status: 'fail' }]);
|
||||
assert.equal(result.ready, false);
|
||||
assert.deepEqual(result.blocking, ['c']);
|
||||
});
|
||||
|
||||
test('SSH helpers produce pinned fingerprints and quoted commands', () => {
|
||||
assert.match(fingerprintKey(Buffer.from('host-key')), /^SHA256:/);
|
||||
assert.equal(shellQuote("a'b"), "'a'\\''b'");
|
||||
const wrapped = bash('git fetch origin main');
|
||||
assert.match(wrapped, /base64 -d \| bash$/);
|
||||
assert.equal(wrapped.includes('\n'), false);
|
||||
const encoded = wrapped.match(/printf '%s' '([A-Za-z0-9+/=]+)'/)[1];
|
||||
const decoded = Buffer.from(encoded, 'base64').toString('utf8');
|
||||
assert.match(decoded, /GIT_TERMINAL_PROMPT=0/);
|
||||
assert.match(decoded, /BatchMode=yes/);
|
||||
assert.match(decoded, /git fetch origin main/);
|
||||
});
|
||||
|
||||
test('SSH rollback refuses any SHA other than the exact recorded previous deployment', async () => {
|
||||
const previousSha = 'a'.repeat(40);
|
||||
const store = {
|
||||
getDeploymentProfile: () => ({ id: 'production', provider: 'ssh-unraid', serverId: 'unraid', remoteFolder: 'lumaops', composeFile: 'docker-compose.yml', branch: 'main', environment: 'production' }),
|
||||
getServer: () => ({ id: 'unraid', basePath: '/mnt/user/appdata' }),
|
||||
getDeploymentState: () => ({ liveSha: 'b'.repeat(40), previousSha })
|
||||
};
|
||||
const service = new UnraidDeploymentService({ store, ssh: {}, git: {}, diagnostics: null });
|
||||
await assert.rejects(
|
||||
service.rollback({ repository: { fullName: 'Jens/LumaOps', name: 'LumaOps', localPath: '/tmp/lumaops' }, profileId: 'production', targetSha: 'c'.repeat(40) }),
|
||||
(error) => error.code === 'ROLLBACK_TARGET_NOT_PREVIOUS_SHA'
|
||||
);
|
||||
});
|
||||
|
||||
test('successful SSH rollback records the formerly live SHA as the new rollback target', async () => {
|
||||
const previousSha = 'a'.repeat(40);
|
||||
const liveSha = 'b'.repeat(40);
|
||||
const savedStates = [];
|
||||
const operations = [];
|
||||
const store = {
|
||||
getDeploymentProfile: () => ({ id: 'production', provider: 'ssh-unraid', serverId: 'unraid', remoteFolder: 'lumaops', composeFile: 'docker-compose.yml', branch: 'main', environment: 'production', healthcheckUrl: '' }),
|
||||
getServer: () => ({ id: 'unraid', name: 'Unraid', basePath: '/mnt/user/appdata' }),
|
||||
getDeploymentState: () => ({ liveSha, previousSha }),
|
||||
addOperation: async (operation) => { operations.push(operation); return operation; },
|
||||
saveDeploymentState: async (_profileId, state) => { savedStates.push(state); return state; }
|
||||
};
|
||||
const git = { verifyCommitOnRemoteBranch: async () => true };
|
||||
const ssh = { exec: async () => ({ stdout: '', stderr: '', exitCode: 0 }) };
|
||||
const service = new UnraidDeploymentService({ store, ssh, git, diagnostics: null });
|
||||
service.inspect = async () => ({ rootGit: true, trackedChanges: [], head: liveSha });
|
||||
service.checkHealth = async () => ({ configured: false, healthy: null, status: null, latencyMs: null });
|
||||
const result = await service.rollback({ repository: { fullName: 'Jens/LumaOps', name: 'LumaOps', localPath: '/tmp/lumaops' }, profileId: 'production', targetSha: previousSha });
|
||||
assert.equal(result.status, 'rolled-back');
|
||||
assert.equal(savedStates.at(-1).liveSha, previousSha);
|
||||
assert.equal(savedStates.at(-1).previousSha, liveSha);
|
||||
assert.equal(operations.at(-1).previousSha, liveSha);
|
||||
});
|
||||
Reference in New Issue
Block a user