Merge branch 'development' into fix/instalacion-desatendida
Conflicto en cras-install.ts por la PR #22, que metió feature/cras-update-in-place en development con hashes distintos de los que esta rama ya traía rebasados. Los dos hunks en conflicto eran la versión ANTERIOR de lo que esta rama sustituye: - probeLinuxElevation sin la contraseña del panel (aquí ya recibe ssh_password y reconoce la elevación sudo-con-password). - La invocación de install.sh sin alimentar el stdin de sudo ni asentar qué vía de elevación se tomó. Resueltos a favor de esta rama, que incorpora lo de development más los cambios propios. Verificado después del merge: 325 pruebas y typecheck limpio. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user