Update
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import deploymentModule from '../src/main/deployment-service.cjs';
|
||||
|
||||
const { DeploymentService, terminalRunConclusion, applicationVerificationFailure } = deploymentModule;
|
||||
|
||||
test('maps runner conclusions to ForgeFlow terminal states', () => {
|
||||
assert.equal(terminalRunConclusion({ conclusion: 'success' }), 'success');
|
||||
assert.equal(terminalRunConclusion({ conclusion: 'failure' }), 'failed');
|
||||
assert.equal(terminalRunConclusion({ status: 'timed_out' }), 'failed');
|
||||
assert.equal(terminalRunConclusion({ conclusion: 'cancelled' }), 'cancelled');
|
||||
assert.equal(terminalRunConclusion({ status: 'running' }), null);
|
||||
});
|
||||
|
||||
|
||||
test('dispatches only controlled deployment inputs', async () => {
|
||||
const sha = 'a'.repeat(40);
|
||||
let dispatched = null;
|
||||
const operations = new Map();
|
||||
const profile = {
|
||||
id: 'staging', name: 'Staging', environment: 'staging', branch: 'main',
|
||||
workflowFile: 'deploy.yml', rollbackWorkflowFile: 'rollback.yml',
|
||||
statusUrl: 'https://app.example.test/.well-known/forgeflow',
|
||||
inputs: { commit_sha: 'b'.repeat(40), request_id: 'forged', arbitrary: 'ignored' }
|
||||
};
|
||||
const store = {
|
||||
getDeploymentProfile: () => profile,
|
||||
getToken: () => '',
|
||||
addOperation: async (operation) => { operations.set(operation.id, structuredClone(operation)); return structuredClone(operation); }
|
||||
};
|
||||
const service = new DeploymentService(store, {
|
||||
dispatchWorkflow: async (payload) => { dispatched = payload; return { accepted: true, status: 204 }; }
|
||||
}, {
|
||||
status: async () => ({ head: sha, clean: true, counts: { changed: 0 }, branch: { head: 'main', upstream: 'origin/main', ahead: 0, behind: 0 } }),
|
||||
verifyCommitOnRemoteBranch: async () => ({ valid: true })
|
||||
}, { info: async () => {}, error: async () => {} });
|
||||
|
||||
const operation = await service.deploy({ repository: { fullName: 'jens/app', localPath: '/repo' }, profileId: profile.id, sha });
|
||||
assert.deepEqual(Object.keys(dispatched.inputs).sort(), ['commit_sha', 'environment', 'request_id']);
|
||||
assert.equal(dispatched.inputs.commit_sha, sha);
|
||||
assert.equal(dispatched.inputs.environment, 'staging');
|
||||
assert.equal(dispatched.inputs.request_id, operation.id);
|
||||
assert.equal(dispatched.inputs.arbitrary, undefined);
|
||||
});
|
||||
|
||||
test('requires exact server SHA and matching request ID after a successful workflow', () => {
|
||||
const operation = { id: 'request-1', repository: 'jens/app', environment: 'staging', sha: 'a'.repeat(40), shortSha: 'aaaaaaa' };
|
||||
assert.equal(applicationVerificationFailure(operation, {
|
||||
statusConfigured: true, statusReachable: true, statusRepository: 'jens/app', statusEnvironment: 'staging',
|
||||
liveSha: operation.sha, requestedSha: operation.sha, requestId: operation.id, lastExitCode: 0, healthy: true
|
||||
}), null);
|
||||
assert.match(applicationVerificationFailure(operation, {
|
||||
statusConfigured: true, statusReachable: true, statusRepository: 'jens/app', statusEnvironment: 'staging',
|
||||
liveSha: operation.sha, requestedSha: operation.sha, requestId: 'another-request', lastExitCode: 0, healthy: true
|
||||
}).message, /different deployment request/i);
|
||||
assert.match(applicationVerificationFailure(operation, {
|
||||
statusConfigured: true, statusReachable: true, statusRepository: 'jens/app', statusEnvironment: 'staging',
|
||||
liveSha: 'b'.repeat(40), requestedSha: operation.sha, requestId: operation.id, lastExitCode: 0, healthy: true
|
||||
}).message, /server reports/i);
|
||||
assert.match(applicationVerificationFailure(operation, {
|
||||
statusConfigured: true, statusReachable: false, liveSha: null,
|
||||
requestId: null, healthy: null
|
||||
}).message, /not reachable/i);
|
||||
assert.match(applicationVerificationFailure(operation, {
|
||||
statusConfigured: true, statusReachable: true, statusRepository: 'other/app', statusEnvironment: 'staging',
|
||||
liveSha: operation.sha, requestedSha: operation.sha, requestId: operation.id, lastExitCode: 0, healthy: true
|
||||
}).message, /belongs to other\/app/i);
|
||||
assert.match(applicationVerificationFailure(operation, {
|
||||
statusConfigured: true, statusReachable: true, statusRepository: 'jens/app', statusEnvironment: 'staging',
|
||||
liveSha: operation.sha, requestedSha: operation.sha, requestId: operation.id, lastExitCode: 70, healthy: false
|
||||
}).message, /exit code 70/i);
|
||||
});
|
||||
|
||||
test('rollback accepts only the currently reported previous SHA and dispatches controlled inputs', async () => {
|
||||
const liveSha = 'a'.repeat(40);
|
||||
const previousSha = 'b'.repeat(40);
|
||||
let dispatched = null;
|
||||
let verified = null;
|
||||
const operations = new Map();
|
||||
const profile = {
|
||||
id: 'production', name: 'Production', environment: 'production', branch: 'main',
|
||||
workflowFile: 'deploy.yml', rollbackWorkflowFile: 'rollback.yml',
|
||||
statusUrl: 'https://app.example.test/.well-known/forgeflow',
|
||||
inputs: { target_sha: 'c'.repeat(40), request_id: 'forged', arbitrary: 'ignored' }
|
||||
};
|
||||
const store = {
|
||||
getDeploymentProfile: () => profile,
|
||||
getToken: () => '',
|
||||
addOperation: async (operation) => { operations.set(operation.id, structuredClone(operation)); return structuredClone(operation); }
|
||||
};
|
||||
const service = new DeploymentService(store, {
|
||||
listWorkflowRuns: async () => ({ runs: [] }),
|
||||
dispatchWorkflow: async (payload) => { dispatched = payload; return { accepted: true, status: 204 }; }
|
||||
}, {
|
||||
verifyCommitOnRemoteBranch: async (...args) => { verified = args; return { valid: true }; }
|
||||
}, { info: async () => {}, warning: async () => {}, error: async () => {} });
|
||||
service.refreshProfileState = async () => ({
|
||||
statusReachable: true,
|
||||
statusRepository: 'jens/app',
|
||||
statusEnvironment: 'production',
|
||||
liveSha,
|
||||
previousSha
|
||||
});
|
||||
|
||||
const operation = await service.rollback({
|
||||
repository: { fullName: 'jens/app', localPath: '/repo' },
|
||||
profileId: profile.id,
|
||||
targetSha: previousSha
|
||||
});
|
||||
|
||||
assert.deepEqual(verified, ['/repo', previousSha, 'main']);
|
||||
assert.deepEqual(Object.keys(dispatched.inputs).sort(), ['environment', 'request_id', 'target_sha']);
|
||||
assert.equal(dispatched.inputs.environment, 'production');
|
||||
assert.equal(dispatched.inputs.target_sha, previousSha);
|
||||
assert.equal(dispatched.inputs.request_id, operation.id);
|
||||
assert.equal(dispatched.inputs.arbitrary, undefined);
|
||||
});
|
||||
|
||||
test('rollback refuses a stale target that is no longer the server-reported previous SHA', async () => {
|
||||
const previousSha = 'b'.repeat(40);
|
||||
let dispatched = false;
|
||||
const profile = {
|
||||
id: 'production', name: 'Production', environment: 'production', branch: 'main',
|
||||
workflowFile: 'deploy.yml', rollbackWorkflowFile: 'rollback.yml',
|
||||
statusUrl: 'https://app.example.test/.well-known/forgeflow'
|
||||
};
|
||||
const service = new DeploymentService({
|
||||
getDeploymentProfile: () => profile,
|
||||
getToken: () => '',
|
||||
addOperation: async (operation) => operation
|
||||
}, {
|
||||
dispatchWorkflow: async () => { dispatched = true; }
|
||||
}, {
|
||||
verifyCommitOnRemoteBranch: async () => ({ valid: true })
|
||||
}, { info: async () => {}, warning: async () => {}, error: async () => {} });
|
||||
service.refreshProfileState = async () => ({
|
||||
statusReachable: true,
|
||||
statusRepository: 'jens/app',
|
||||
statusEnvironment: 'production',
|
||||
liveSha: 'a'.repeat(40),
|
||||
previousSha
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
service.rollback({
|
||||
repository: { fullName: 'jens/app', localPath: '/repo' },
|
||||
profileId: profile.id,
|
||||
targetSha: 'c'.repeat(40)
|
||||
}),
|
||||
/no longer the previous server version/i
|
||||
);
|
||||
assert.equal(dispatched, false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user