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