// Die MakeMKV-Beschaffungskette (§ 9) — Fixtures aus den Live-Messungen // vom 31.08.2026: Hersteller-Seite HTTP 525, Forum 1.18.4 in 0,2 s, // Archiv 1.18.2 in 7,8 s. import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, describe, expect, it } from 'vitest' import { dateiPruefen, hoechsteVersion, istNeuer, makemkvInstallerHolen, makemkvNeuesteVersion, makemkvQuellen, versionsquellen, } from '../src/kern/werkzeuge/beschaffen' import { istFensterNachricht, istHauptNachricht, istKernNachricht } from '../src/gemeinsam/nachrichten' const aufraeumen: string[] = [] afterEach(() => { for (const ordner of aufraeumen.splice(0).reverse()) { rmSync(ordner, { recursive: true, force: true }) } }) function tempOrdner(): string { const ordner = mkdtempSync(join(tmpdir(), 'rippy-beschaffung-')) aufraeumen.push(ordner) return ordner } function antwort(body: string | Buffer, status = 200): Response { return new Response(typeof body === 'string' ? body : new Uint8Array(body), { status }) } describe('hoechsteVersion', () => { it('vergleicht numerisch — als Zeichenkette wäre 1.9.2 größer als 1.18.4', () => { expect(hoechsteVersion('MakeMKV v1.9.2 … MakeMKV 1.18.4 … MakeMKV v1.18.2')).toBe('1.18.4') }) it('leerer Text → leere Antwort', () => { expect(hoechsteVersion('')).toBe('') expect(hoechsteVersion('nichts hier')).toBe('') }) }) describe('istNeuer', () => { it('numerisch je Stelle', () => { expect(istNeuer('1.18.4', '1.18.2')).toBe(true) expect(istNeuer('1.9.2', '1.18.4')).toBe(false) expect(istNeuer('1.18.4', '1.18.4')).toBe(false) expect(istNeuer('1.18.4.1', '1.18.4')).toBe(true) }) it('Unbekanntes ist nie neuer — außer gegenüber gar nichts', () => { expect(istNeuer('', '1.0.0')).toBe(false) expect(istNeuer('1.0.0', '')).toBe(true) }) }) describe('Quellenketten', () => { it('Versionsquellen: der Hersteller zuerst, und nur er ist maßgeblich', () => { const quellen = versionsquellen() expect(quellen).toHaveLength(3) expect(quellen[0].name).toBe('Hersteller-Seite') expect(quellen[0].massgeblich).toBe(true) expect(quellen.slice(1).every((q) => !q.massgeblich)).toBe(true) // Zeitstempel in der Vergangenheit — sonst holt das Archiv live nach. expect(quellen[2].url).toContain('web.archive.org/web/2025/') }) it('Datei-Quellen: Hersteller → Archiv (mit id_ für die ROHE Datei)', () => { const quellen = makemkvQuellen('1.18.4') expect(quellen.map(([name]) => name)).toEqual(['Hersteller', 'Internet Archive']) expect(quellen[0][1]).toBe('https://www.makemkv.com/download/Setup_MakeMKV_v1.18.4.exe') expect(quellen[1][1]).toBe( 'https://web.archive.org/web/2025id_/https://www.makemkv.com/download/Setup_MakeMKV_v1.18.4.exe', ) }) it('eine eigene Basis steht davor — wer sie einträgt, will sie', () => { const quellen = makemkvQuellen('1.18.4', 'https://mein-server.example/mkv/') expect(quellen[0]).toEqual(['eigene Quelle', 'https://mein-server.example/mkv/Setup_MakeMKV_v1.18.4.exe']) expect(quellen).toHaveLength(3) }) }) describe('makemkvNeuesteVersion', () => { it('der Hersteller ist maßgeblich — antwortet er, wird nicht weitergefragt', async () => { const gefragt: string[] = [] const holen = (async (url: string | URL | Request) => { gefragt.push(String(url)) return antwort('MakeMKV 1.19.0 is out') }) as typeof fetch expect(await makemkvNeuesteVersion(holen)).toBe('1.19.0') expect(gefragt).toHaveLength(1) }) it('Hersteller tot (525, wie gemessen) → höchste aus Forum und Archiv', async () => { const holen = (async (url: string | URL | Request) => { const wo = String(url) if (wo.includes('www.makemkv.com')) return antwort('cloudflare', 525) if (wo.includes('forum')) return antwort('MakeMKV v1.18.4 released') return antwort('MakeMKV v1.18.2 … MakeMKV v1.18.1') }) as typeof fetch expect(await makemkvNeuesteVersion(holen)).toBe('1.18.4') }, 15_000) it('keine Quelle → leere Antwort statt Wurf', async () => { expect(await makemkvNeuesteVersion(undefined, [])).toBe('') }) }) describe('dateiPruefen', () => { it('ohne MZ-Kopf ist es kein Windows-Programm', () => { const ordner = tempOrdner() const pfad = join(ordner, 'seite.exe') writeFileSync(pfad, 'Fehlerseite') const urteil = dateiPruefen(pfad, '1.18.4') expect(urteil.ok).toBe(false) expect(urteil.grund).toContain('MZ') }) it('unter Windows zählt die Versions-Ressource (gemessene Felder)', () => { const ordner = tempOrdner() const pfad = join(ordner, 'setup.exe') writeFileSync(pfad, 'MZ\x90rest') // Die am 31.08.2026 an der echten Datei gemessenen Werte: const echt = { firma: 'GuinpinSoft inc', produkt: 'MakeMKV', version: 'v1.18.4' } expect(dateiPruefen(pfad, '1.18.4', () => echt, 'win32').ok).toBe(true) expect(dateiPruefen(pfad, '1.18.4', () => ({ ...echt, firma: 'Boese GmbH' }), 'win32').ok).toBe(false) expect(dateiPruefen(pfad, '1.19.0', () => echt, 'win32').ok).toBe(false) // Auf Linux (CI) gibt es keine Versions-Ressourcen — MZ-Test muss reichen. expect(dateiPruefen(pfad, '1.18.4', () => echt, 'linux').ok).toBe(true) }) }) describe('makemkvInstallerHolen', () => { const GROSS = Buffer.concat([Buffer.from('MZ'), Buffer.alloc(1024 * 1024 + 100, 7)]) it('Fehlerseite mit HTTP 200 fällt durch die Mindestgröße — die nächste Quelle gewinnt (mit Freigabe)', async () => { const ordner = tempOrdner() const holen = (async (url: string | URL | Request) => String(url).includes('web.archive.org') ? antwort(GROSS) : antwort('kaputt')) as typeof fetch const gefragt: string[] = [] const ergebnis = await makemkvInstallerHolen( '1.18.4', () => {}, '', holen, ordner, (p, v) => dateiPruefen(p, v, () => ({ firma: 'GuinpinSoft inc', produkt: 'MakeMKV', version: 'v1.18.4' }), 'win32'), async (quelle) => { gefragt.push(quelle) return true }, ) expect(gefragt).toEqual(['Internet Archive']) expect(ergebnis.quelle).toBe('Internet Archive') expect(readFileSync(ergebnis.pfad).length).toBe(GROSS.length) expect(ergebnis.bytes).toBe(GROSS.length) expect(ergebnis.sha256).toMatch(/^[0-9a-f]{64}$/) expect(existsSync(`${ergebnis.pfad}.neu`)).toBe(false) }) // 5.7.0 (Durchsicht F16): Ohne Freigabe wird das Archiv NIE benutzt — es // liefert eine unsignierte EXE, die mit Adminrechten läuft. it('ohne Freigabe bleibt das Archiv aus — und der Bericht sagt es', async () => { const ordner = tempOrdner() const holen = (async (url: string | URL | Request) => String(url).includes('web.archive.org') ? antwort(GROSS) : antwort('kaputt')) as typeof fetch await expect(makemkvInstallerHolen('1.18.4', () => {}, '', holen, ordner)).rejects.toThrow(/Internet Archive: nicht freigegeben/) await expect(makemkvInstallerHolen('1.18.4', () => {}, '', holen, ordner, undefined, async () => false)).rejects.toThrow( /Internet Archive: nicht freigegeben/, ) }) it('liefert keine Quelle, steht im Wurf der Bericht ALLER Versuche', async () => { const ordner = tempOrdner() const holen = (async () => antwort('weg', 404)) as typeof fetch await expect(makemkvInstallerHolen('1.18.4', () => {}, '', holen, ordner, undefined, async () => true)).rejects.toThrow( /Hersteller: HTTP 404[\s\S]*Internet Archive: HTTP 404[\s\S]*makemkv\.com/, ) }) }) describe('Nachrichten-Schema der Update-Strecke', () => { it('Fenster → Kern: makemkv-update und schluessel-pruefen', () => { expect(istFensterNachricht({ art: 'makemkv-update' })).toBe(true) expect(istFensterNachricht({ art: 'schluessel-pruefen' })).toBe(true) expect(istFensterNachricht({ art: 'beschaffung-antwort', ja: true })).toBe(true) expect(istFensterNachricht({ art: 'beschaffung-antwort' })).toBe(false) }) it('Haupt → Kern: installer-ergebnis', () => { expect(istHauptNachricht({ art: 'installer-ergebnis', fehler: null })).toBe(true) expect(istHauptNachricht({ art: 'installer-ergebnis', fehler: 'UAC abgelehnt' })).toBe(true) expect(istHauptNachricht({ art: 'installer-ergebnis', fehler: 5 })).toBe(false) }) it('Kern → Haupt/Fenster: beschaffung-status und installer-starten', () => { expect( istKernNachricht({ art: 'beschaffung-status', stand: { werkzeug: 'makemkv', laeuft: true, text: 'lädt', anteil: 0.5, fehler: false }, }), ).toBe(true) expect(istKernNachricht({ art: 'installer-starten', pfad: 'C:\\x\\setup.exe' })).toBe(true) expect(istKernNachricht({ art: 'installer-starten', pfad: '' })).toBe(false) }) })