/** * Comparación de versiones y normalización de plataforma para CloudRestoreAS. * * Es la lógica que decide si a un servidor le falta actualizarse, así que los casos que se * prueban son los que romperían eso en silencio: comparación lexicográfica (1.10 vs 1.9), * versiones de distinta longitud, y valores no comparables. */ import { describe, expect, it } from 'vitest'; import { compareVersions, effectiveArch, effectivePlatform, installPlatformVerdict, isCrasPlatform, isInsideWorkFolder, isNewer, isValidVersion, osToPlatform, parseVersion, platformLabel, DEFAULT_ARCH } from './cras-version'; describe('isValidVersion', () => { it('acepta versiones de puntos y números', () => { for (const v of ['1', '1.0', '1.1.0', '26.7.1.4']) { expect(isValidVersion(v), v).toBe(true); } }); it('rechaza lo que el PANEL no podría ordenar', () => { for (const v of ['1.0.0-rc1', 'v1.0.0', '1.0.0a', '', ' ', 'latest', '1.2.3.4.5']) { expect(isValidVersion(v), String(v)).toBe(false); } }); it('rechaza valores que no son cadena', () => { expect(isValidVersion(null)).toBe(false); expect(isValidVersion(undefined)).toBe(false); expect(isValidVersion(110)).toBe(false); }); }); describe('parseVersion', () => { it('descompone en enteros', () => { expect(parseVersion('1.10.2')).toEqual([1, 10, 2]); expect(parseVersion(' 2.0 ')).toEqual([2, 0]); }); it('devuelve null si no es comparable', () => { expect(parseVersion('1.0.0-rc1')).toBeNull(); }); }); describe('compareVersions', () => { it('compara numéricamente, no como texto', () => { // El error clásico: como cadenas, "1.9.0" > "1.10.0". expect(compareVersions('1.10.0', '1.9.0')).toBe(1); expect(compareVersions('1.9.0', '1.10.0')).toBe(-1); }); it('rellena con ceros las de distinta longitud', () => { expect(compareVersions('1.2', '1.2.0')).toBe(0); expect(compareVersions('1.2.1', '1.2')).toBe(1); expect(compareVersions('2', '1.9.9.9')).toBe(1); }); it('es simétrica', () => { expect(compareVersions('1.1.0', '1.1.0')).toBe(0); expect(compareVersions('26.7.1.4', '26.7.1.3')).toBe(1); expect(compareVersions('26.7.1.3', '26.7.1.4')).toBe(-1); }); it('devuelve null cuando alguna no es comparable', () => { expect(compareVersions('1.0.0-rc1', '1.0.0')).toBeNull(); expect(compareVersions('1.0.0', 'latest')).toBeNull(); }); }); describe('isNewer', () => { it('detecta una versión más nueva', () => { expect(isNewer('1.2.0', '1.1.0')).toBe(true); expect(isNewer('1.10.0', '1.9.0')).toBe(true); }); it('no ofrece downgrade ni reinstalación de la misma', () => { expect(isNewer('1.1.0', '1.2.0')).toBe(false); expect(isNewer('1.1.0', '1.1.0')).toBe(false); }); it('sin versión instalada, hay algo que instalar', () => { expect(isNewer('1.1.0', null)).toBe(true); expect(isNewer('1.1.0', undefined)).toBe(true); expect(isNewer('1.1.0', ' ')).toBe(true); }); it('ante una versión no comparable prefiere NO ofrecer nada', () => { // Preferible perderse una actualización que empujar un downgrade por un dato sucio. expect(isNewer('1.0.0-rc1', '1.0.0')).toBe(false); expect(isNewer('1.2.0', 'desconocida')).toBe(false); }); }); describe('osToPlatform', () => { it('reconoce Windows en texto libre', () => { for (const os of ['Windows Server 2019', 'windows 11', 'WIN SERVER 2022', 'Win']) { expect(osToPlatform(os), os).toBe('windows'); } }); it('reconoce distribuciones de Linux', () => { for (const os of [ 'Ubuntu 22.04', 'Debian 12', 'CentOS 7', 'RHEL 9', 'Red Hat Enterprise', 'Rocky Linux 9', 'AlmaLinux', 'SUSE', 'Oracle Linux 8' ]) { expect(osToPlatform(os), os).toBe('linux'); } }); it('devuelve null si no puede decidir, en lugar de adivinar', () => { // Adivinar mandaría el binario equivocado; la UI pide elegir a mano. for (const os of ['', ' ', null, undefined, 'servidor de la sucursal', 'macOS 14']) { expect(osToPlatform(os as string), String(os)).toBeNull(); } }); }); describe('effectivePlatform', () => { it('lo reportado por el agente manda sobre el texto libre', () => { expect(effectivePlatform('linux', 'Windows Server 2019')).toBe('linux'); expect(effectivePlatform('windows', 'Ubuntu 22.04')).toBe('windows'); }); it('cae al texto libre cuando el agente no reportó', () => { expect(effectivePlatform(null, 'Ubuntu 22.04')).toBe('linux'); expect(effectivePlatform(' ', 'Windows Server 2022')).toBe('windows'); }); it('ignora una plataforma reportada inválida', () => { expect(effectivePlatform('solaris', 'Ubuntu 22.04')).toBe('linux'); expect(effectivePlatform('solaris', null)).toBeNull(); }); }); describe('effectiveArch', () => { it('usa la reportada y normaliza a minúsculas', () => { expect(effectiveArch('X86_64')).toBe('x86_64'); expect(effectiveArch('arm64')).toBe('arm64'); }); it('asume la única arquitectura que se publica hoy si falta', () => { expect(effectiveArch(null)).toBe(DEFAULT_ARCH); expect(effectiveArch(' ')).toBe(DEFAULT_ARCH); }); }); describe('isCrasPlatform y platformLabel', () => { it('valida el vocabulario del CHECK de cras_releases', () => { expect(isCrasPlatform('windows')).toBe(true); expect(isCrasPlatform('linux')).toBe(true); expect(isCrasPlatform('solaris')).toBe(false); expect(isCrasPlatform(null)).toBe(false); }); it('etiqueta legible, incluso sin dato', () => { expect(platformLabel('windows')).toBe('Windows'); expect(platformLabel('linux')).toBe('Linux'); expect(platformLabel(null)).toBe('Sin determinar'); }); }); describe('installPlatformVerdict', () => { it('con plataforma conocida solo deja pasar el artefacto que corresponde', () => { expect(installPlatformVerdict('linux', 'linux', null)).toBe('ok'); expect(installPlatformVerdict('windows', 'linux', null)).toBe('mismatch'); expect(installPlatformVerdict('linux', 'windows', null)).toBe('mismatch'); }); it('una confirmación no puede pasar por encima de una plataforma conocida', () => { // El destino es Windows y el operador confirma "linux": sigue siendo un desajuste. expect(installPlatformVerdict('windows', 'linux', 'linux')).toBe('mismatch'); }); it('sin plataforma determinada exige confirmación explícita del operador', () => { // El defecto que se corrige: el modal preseleccionaba el primer artefacto del catálogo // (el más recién descubierto, hoy Linux) para un servidor "Sin determinar". expect(installPlatformVerdict(null, 'linux', null)).toBe('needs-ack'); expect(installPlatformVerdict(null, 'linux', '')).toBe('needs-ack'); expect(installPlatformVerdict(undefined, 'windows', undefined)).toBe('needs-ack'); }); it('la confirmación tiene que coincidir con la plataforma del artefacto', () => { expect(installPlatformVerdict(null, 'linux', 'linux')).toBe('ok'); expect(installPlatformVerdict(null, 'linux', 'windows')).toBe('needs-ack'); expect(installPlatformVerdict(null, 'windows', ' WINDOWS ')).toBe('ok'); }); }); /** * Guard de la ruta de instalación contra las carpetas de trabajo del agente. * * No tenía ni una prueba, y con la relación mirada en los dos sentidos rechazaba el layout * NORMAL —`DIR_ENTRADA = APP_DIR / "Entrada"` en constants.py del agente— así que ninguna * instalación ni actualización podía pasar. Se salvaban solo las instalaciones fuera de la ruta * por omisión, por accidente del nombre. */ describe('isInsideWorkFolder', () => { it('acepta el layout normal: las carpetas de trabajo cuelgan de la de instalación', () => { expect( isInsideWorkFolder('/opt/cloudrestoreas', [ '/opt/cloudrestoreas/Entrada', '/opt/cloudrestoreas/Procesados' ]) ).toBe(false); expect( isInsideWorkFolder('C:\\Aduanasoft\\CloudRestoreAS', [ 'C:\\Aduanasoft\\CloudRestoreAS\\Entrada' ]) ).toBe(false); }); it('rechaza el binario DENTRO de una carpeta de trabajo, que es el peligro real', () => { // Ahí el agente tomaría su propio ejecutable por un respaldo a procesar. expect(isInsideWorkFolder('/srv/entrada/cras', ['/srv/entrada'])).toBe(true); expect( isInsideWorkFolder('D:\\Respaldos\\Entrada\\app', ['D:\\Respaldos\\Entrada']) ).toBe(true); }); it('rechaza instalar exactamente EN la carpeta de trabajo', () => { expect(isInsideWorkFolder('/srv/entrada', ['/srv/entrada'])).toBe(true); expect(isInsideWorkFolder('/srv/entrada/', ['/srv/entrada'])).toBe(true); }); it('normaliza separadores, barra final y caja antes de comparar', () => { expect(isInsideWorkFolder('C:/Respaldos/Entrada/app', ['c:\\respaldos\\entrada\\'])).toBe( true ); }); it('un prefijo que no es de carpeta no cuenta como estar dentro', () => { // Es el caso que dejaba pasar a Principal: -win rompe el prefijo de carpeta. expect( isInsideWorkFolder('C:\\Aduanasoft\\CloudRestoreAS', [ 'C:\\Aduanasoft\\CloudRestoreAS-win\\Entrada' ]) ).toBe(false); expect(isInsideWorkFolder('/srv/entradas', ['/srv/entrada'])).toBe(false); }); it('ignora carpetas vacías o sin reportar en lugar de bloquear', () => { expect(isInsideWorkFolder('/opt/cloudrestoreas', [null, undefined, '', ' '])).toBe(false); expect(isInsideWorkFolder('', ['/srv/entrada'])).toBe(false); }); });