fix/instalacion-desatendida #23

Merged
acazares merged 11 commits from fix/instalacion-desatendida into development 2026-07-31 15:00:06 +00:00
Showing only changes of commit e98414456c - Show all commits

View File

@@ -579,3 +579,106 @@ describe('probeInPlaceUpdate', () => {
}
});
});
/**
* Precondiciones de la actualización en sitio sin privilegios.
*
* Cada rechazo tiene un remedio distinto, y por eso no basta un booleano. Lo que se prueba aquí
* son los NO: el sí es trivial y el daño está en aceptar un servidor donde no se puede.
*/
describe('probeInPlaceUpdate', () => {
const PREFIX = '/opt/cloudrestoreas';
function sftpFor(responses: Record<string, { code?: number; stdout?: string; stderr?: string }>) {
return fakeSftp((command) => {
for (const [needle, result] of Object.entries(responses)) {
if (command.includes(needle)) return result;
}
return { code: 0, stdout: '' };
});
}
/** Destino ideal: ruta del usuario, unit suyo con Restart=always, sin jobs. */
const OK = {
'test -w': { code: 0, stdout: 'si' },
'systemctl show': { code: 0, stdout: 'User=srvmid_db\nRestart=always' },
'/Temp': { code: 0, stdout: '' }
};
it('acepta cuando se cumplen las tres condiciones', async () => {
const { sftp } = sftpFor(OK);
expect(await probeInPlaceUpdate(sftp as never, PREFIX, 'srvmid_db')).toMatchObject({ ok: true });
});
it('rechaza si el directorio no es escribible, y dice de quién es', async () => {
// Sin escritura en el DIRECTORIO no se puede reemplazar el binario, y es el único
// permiso que hace falta: `install` desvincula antes de crear.
const { sftp } = sftpFor({
...OK,
'test -w': { code: 1, stdout: 'no' },
'stat -c': { code: 0, stdout: 'root' }
});
const r = await probeInPlaceUpdate(sftp as never, PREFIX, 'srvmid_db');
expect(r.ok).toBe(false);
expect(r.reason).toContain('root');
expect(r.reason).toContain(PREFIX);
});
it('rechaza si el servicio corre como otro usuario', async () => {
// Si el unit quedó con User=root —alguien instaló desde un `sudo -i`— la cuenta SSH no
// puede señalizar el proceso y toda la vía se cae.
const { sftp } = sftpFor({
...OK,
'systemctl show': { code: 0, stdout: 'User=root\nRestart=always' }
});
const r = await probeInPlaceUpdate(sftp as never, PREFIX, 'srvmid_db');
expect(r.ok).toBe(false);
expect(r.reason).toMatch(/corre como 'root'/);
});
it('trata el User vacío como root, que es lo que hace systemd', async () => {
const { sftp } = sftpFor({
...OK,
'systemctl show': { code: 0, stdout: 'User=\nRestart=always' }
});
const r = await probeInPlaceUpdate(sftp as never, PREFIX, 'srvmid_db');
expect(r.ok).toBe(false);
expect(r.reason).toMatch(/root/);
});
it('rechaza sin Restart=always: matarlo lo dejaría muerto', async () => {
for (const restart of ['on-failure', 'no', '']) {
const { sftp } = sftpFor({
...OK,
'systemctl show': { code: 0, stdout: `User=srvmid_db\nRestart=${restart}` }
});
const r = await probeInPlaceUpdate(sftp as never, PREFIX, 'srvmid_db');
expect(r.ok, `Restart=${restart}`).toBe(false);
expect(r.reason).toMatch(/Restart=always/);
}
});
it('rechaza si hay una restauración en curso, antes de subir 270 MB', async () => {
// El mismo chequeo lo repite install.sh en el destino, pero hacerlo aquí evita la
// transferencia completa para nada.
const { sftp } = sftpFor({ ...OK, '/Temp': { code: 0, stdout: 'job-4212' } });
const r = await probeInPlaceUpdate(sftp as never, PREFIX, 'srvmid_db');
expect(r.ok).toBe(false);
expect(r.reason).toMatch(/restauración en curso/);
});
it('el motivo del rechazo nunca queda vacío: cada caso tiene su remedio', async () => {
const casos = [
{ ...OK, 'test -w': { code: 1, stdout: 'no' } },
{ ...OK, 'systemctl show': { code: 0, stdout: 'User=otro\nRestart=always' } },
{ ...OK, 'systemctl show': { code: 0, stdout: 'User=srvmid_db\nRestart=no' } },
{ ...OK, '/Temp': { code: 0, stdout: 'job-1' } }
];
for (const caso of casos) {
const { sftp } = sftpFor(caso);
const r = await probeInPlaceUpdate(sftp as never, PREFIX, 'srvmid_db');
expect(r.ok).toBe(false);
expect(r.reason.trim().length).toBeGreaterThan(20);
}
});
});