import 'reflect-metadata'; import * as crypto from 'crypto'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { NestFactory } from '@nestjs/core'; import { BadRequestException, NotFoundException } from '@nestjs/common'; import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { IS_PUBLIC_KEY } from '../auth/decorators/public.decorator'; import { DesktopController } from './desktop.controller'; import { DesktopModule } from './desktop.module'; import { DesktopService } from './desktop.service'; /** * DesktopService/DesktopController.spec — HTTP-Durchstich ueber * NestFactory (Phase 18, Task 1). Kein `fs`-Mock: ein echtes * Temp-Verzeichnis mit einer kleinen Zufallsdatei und einem von Hand * geschriebenen manifest.json, dessen sha256 unabhaengig ueber * crypto.createHash berechnet wird -- der Pruefling erzeugt den * Erwartungswert nicht selbst. * * `DesktopService.getManifest()` liest manifest.json bei JEDEM Aufruf neu * (kein Cache) -- writeManifest() darf die Datei deshalb zwischen Tests * ueberschreiben, ohne den laufenden HTTP-Server neu zu starten. */ const ORIGINAL_ENV = process.env.DESKTOP_DIST_DIR; let tempDir: string; let app: Awaited>; let baseUrl: string; const PACKAGE_NAME = 'test-package.bin'; let packageSize: number; let packageSha256: string; function writeManifest(files: Record) { fs.writeFileSync( path.join(tempDir, 'manifest.json'), JSON.stringify({ version: '1.1.0', channel: 'dev', commit: 'abc1234', buildTime: '2026-09-16T00:00:00Z', files, }), ); } beforeAll(async () => { tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tessera-desktop-')); const packageBytes = crypto.randomBytes(64 * 1024); fs.writeFileSync(path.join(tempDir, PACKAGE_NAME), packageBytes); packageSize = packageBytes.length; packageSha256 = crypto.createHash('sha256').update(packageBytes).digest('hex'); writeManifest({ linux: { name: PACKAGE_NAME, size: packageSize, sha256: packageSha256 } }); process.env.DESKTOP_DIST_DIR = tempDir; app = await NestFactory.create(DesktopModule, { logger: false }); await app.listen(0); const address = app.getHttpServer().address(); const port = typeof address === 'object' && address ? address.port : 0; baseUrl = `http://127.0.0.1:${port}`; }); afterAll(async () => { await app.close(); fs.rmSync(tempDir, { recursive: true, force: true }); if (ORIGINAL_ENV === undefined) { delete process.env.DESKTOP_DIST_DIR; } else { process.env.DESKTOP_DIST_DIR = ORIGINAL_ENV; } }); afterEach(() => { // Default-Manifest fuer den naechsten Test wiederherstellen (Tests 6/7 // ueberschreiben es bewusst mit einer anderen Form). writeManifest({ linux: { name: PACKAGE_NAME, size: packageSize, sha256: packageSha256 } }); }); describe('DesktopService/DesktopController — HTTP-Durchstich (Phase 18)', () => { it('Test 1 (latest, Manifest vorhanden): 200 mit Kopf-Feldern und relativer Download-URL', async () => { const res = await fetch(`${baseUrl}/desktop/latest`); expect(res.status).toBe(200); const body = await res.json(); expect(body).toEqual({ version: '1.1.0', channel: 'dev', commit: 'abc1234', buildTime: '2026-09-16T00:00:00Z', files: { linux: { name: PACKAGE_NAME, size: packageSize, sha256: packageSha256, url: '/desktop/download/linux', }, }, }); }); it('Test 2 (getLatest ohne Manifest): eigene Instanz mit leerem Temp-Verzeichnis wirft NotFoundException', () => { const emptyDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tessera-desktop-empty-')); const previous = process.env.DESKTOP_DIST_DIR; process.env.DESKTOP_DIST_DIR = emptyDir; try { const service = new DesktopService(); expect(() => service.getLatest()).toThrow(NotFoundException); } finally { process.env.DESKTOP_DIST_DIR = previous; fs.rmSync(emptyDir, { recursive: true, force: true }); } }); it('Test 3 (download/linux): 200, attachment-Header und Body-Hash stimmen mit dem Manifest ueberein', async () => { const res = await fetch(`${baseUrl}/desktop/download/linux`); expect(res.status).toBe(200); expect(res.headers.get('content-disposition')).toBe(`attachment; filename="${PACKAGE_NAME}"`); expect(res.headers.get('content-type')).toBe('application/octet-stream'); expect(res.headers.get('content-length')).toBe(String(packageSize)); const buffer = Buffer.from(await res.arrayBuffer()); const hash = crypto.createHash('sha256').update(buffer).digest('hex'); expect(hash).toBe(packageSha256); }); it('Test 4 (Plattform-Whitelist + Traversal ueber HTTP): mac und ..%2F..%2Fetc%2Fpasswd enden mit 400', async () => { const resMac = await fetch(`${baseUrl}/desktop/download/mac`); expect(resMac.status).toBe(400); const resTraversal = await fetch(`${baseUrl}/desktop/download/..%2F..%2Fetc%2Fpasswd`); expect(resTraversal.status).toBe(400); }); it('Test 5 (Whitelist vor Dateisystem): nicht existierendes Verzeichnis + mac wirft BadRequestException, nicht NotFoundException', () => { const missingDir = path.join(os.tmpdir(), 'tessera-desktop-does-not-exist-' + Date.now()); const previous = process.env.DESKTOP_DIST_DIR; process.env.DESKTOP_DIST_DIR = missingDir; try { const service = new DesktopService(); expect(() => service.getPackage('mac')).toThrow(BadRequestException); } finally { process.env.DESKTOP_DIST_DIR = previous; } }); it('Test 6 (Manifest nur mit windows): download/linux endet mit 404', async () => { writeManifest({ windows: { name: 'Tessera-Setup-1.1.0.exe', size: 123, sha256: 'a'.repeat(64) }, }); const res = await fetch(`${baseUrl}/desktop/download/linux`); expect(res.status).toBe(404); }); it('Test 7 (manipulierter Name im Manifest): "../x.AppImage" endet mit 404', async () => { writeManifest({ linux: { name: '../x.AppImage', size: 123, sha256: 'a'.repeat(64) }, }); const res = await fetch(`${baseUrl}/desktop/download/linux`); expect(res.status).toBe(404); }); it('Test 8 (bewusst oeffentlich): getLatest und download tragen @Public()', () => { expect(Reflect.getMetadata(IS_PUBLIC_KEY, DesktopController.prototype.getLatest)).toBe(true); expect(Reflect.getMetadata(IS_PUBLIC_KEY, DesktopController.prototype.download)).toBe(true); }); });