import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest'; import { FAVORITE_ICON_MAX_BYTES, detectFavoriteIconMime, favoriteIconAbsolutePath, favoriteIconExtension, removeFavoriteIconFileBestEffort, resolveFavoriteIconsDir, } from './favorite-icon-files'; /** * favorite-icon-files.spec — NEU (quick-260923-lrr). * * Erkennung (Muster dashboard-image-rules.spec.ts): PNG/JPEG/GIF/WebP wie * `detectImageMime`, dazu ICO (Kopfstueck, kein CUR) und SVG (Praefix-Form, * kein `` davor). Pfadbildung: Endung aus dem Typ, Segmente nur aus * Buchstaben/Ziffern/Bindestrich, Ergebnis muss im Symbolverzeichnis liegen * (T-LRR-01). `FAVORITE_ICONS_DIR` steuert das Verzeichnis in Tests, wie * `DASHBOARD_IMAGES_DIR` es fuer die Bilderrahmen-Bilder tut. */ function bytes(...parts: (number[] | string)[]): Uint8Array { const out: number[] = []; for (const p of parts) { if (typeof p === 'string') { for (const ch of p) out.push(ch.charCodeAt(0)); } else { out.push(...p); } } return Uint8Array.from(out); } describe('detectFavoriteIconMime (quick-260923-lrr)', () => { it('PNG/JPEG/GIF/WebP-Signaturen ergeben dieselben Typen wie detectImageMime', () => { expect( detectFavoriteIconMime(bytes([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], [0, 0, 0, 13])), ).toBe('image/png'); expect(detectFavoriteIconMime(bytes([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10], 'JFIF'))).toBe('image/jpeg'); expect(detectFavoriteIconMime(bytes('GIF89a', [1, 0, 1, 0]))).toBe('image/gif'); expect(detectFavoriteIconMime(bytes('RIFF', [0x24, 0x00, 0x00, 0x00], 'WEBP', 'VP8 '))).toBe( 'image/webp', ); }); it('Bytes 00 00 01 00 (mindestens 6 Bytes) ergeben image/x-icon', () => { expect(detectFavoriteIconMime(bytes([0x00, 0x00, 0x01, 0x00, 0x01, 0x00]))).toBe('image/x-icon'); }); it('zu kurzes ICO-Kopfstueck (weniger als 6 Bytes) ergibt null', () => { expect(detectFavoriteIconMime(bytes([0x00, 0x00, 0x01, 0x00]))).toBeNull(); }); it('00 00 02 00 (CUR-Cursor-Datei) ergibt null — nur Typ 1 (ICO) wird erkannt', () => { expect(detectFavoriteIconMime(bytes([0x00, 0x00, 0x02, 0x00, 0x01, 0x00]))).toBeNull(); }); it('gueltige SVG-Formen ergeben image/svg+xml', () => { expect(detectFavoriteIconMime(bytes(''))).toBe( 'image/svg+xml', ); expect( detectFavoriteIconMime(bytes('\n')), ).toBe('image/svg+xml'); // fuehrendes BOM expect( detectFavoriteIconMime(bytes([0xef, 0xbb, 0xbf], '')), ).toBe('image/svg+xml'); // fuehrendes Leerzeichen expect(detectFavoriteIconMime(bytes(' '))).toBe('image/svg+xml'); // Kommentar vor \n')), ).toBe('image/svg+xml'); // DOCTYPE svg vor \n', ), ), ).toBe('image/svg+xml'); }); it('ungueltige Formen ergeben null', () => { expect(detectFavoriteIconMime(bytes(''))).toBeNull(); expect(detectFavoriteIconMime(bytes('\n'))).toBeNull(); expect(detectFavoriteIconMime(new Uint8Array(0))).toBeNull(); expect(detectFavoriteIconMime(bytes('Dies ist keine Bilddatei, sondern Text.'))).toBeNull(); expect(detectFavoriteIconMime(bytes('%PDF-1.7\n%\xe2\xe3'))).toBeNull(); expect(detectFavoriteIconMime(bytes([0x00, 0x00, 0x02, 0x00, 0x01, 0x00]))).toBeNull(); }); }); describe('favoriteIconExtension (quick-260923-lrr)', () => { it('bildet die sechs bekannten Typen ab, unbekannter Typ ergibt null', () => { expect(favoriteIconExtension('image/png')).toBe('png'); expect(favoriteIconExtension('image/jpeg')).toBe('jpg'); expect(favoriteIconExtension('image/gif')).toBe('gif'); expect(favoriteIconExtension('image/webp')).toBe('webp'); expect(favoriteIconExtension('image/x-icon')).toBe('ico'); expect(favoriteIconExtension('image/svg+xml')).toBe('svg'); expect(favoriteIconExtension('application/pdf')).toBeNull(); }); }); describe('resolveFavoriteIconsDir / favoriteIconAbsolutePath (quick-260923-lrr)', () => { let dir: string; const ORIGINAL_ENV = process.env.FAVORITE_ICONS_DIR; beforeAll(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tessera-favorite-icons-')); process.env.FAVORITE_ICONS_DIR = dir; }); afterAll(() => { fs.rmSync(dir, { recursive: true, force: true }); if (ORIGINAL_ENV === undefined) { delete process.env.FAVORITE_ICONS_DIR; } else { process.env.FAVORITE_ICONS_DIR = ORIGINAL_ENV; } }); it('resolveFavoriteIconsDir beachtet FAVORITE_ICONS_DIR', () => { expect(resolveFavoriteIconsDir()).toBe(path.resolve(dir)); }); it('liegt unter resolveFavoriteIconsDir()//.', () => { const result = favoriteIconAbsolutePath('user-1', 'fav-1', 'image/png'); expect(result).toBe(path.join(resolveFavoriteIconsDir(), 'user-1', 'fav-1.png')); }); it('unbekannter Typ ergibt null', () => { expect(favoriteIconAbsolutePath('user-1', 'fav-1', 'application/pdf')).toBeNull(); }); it('Segmente mit .., /, \\ oder leer ergeben null', () => { expect(favoriteIconAbsolutePath('..', 'fav-1', 'image/png')).toBeNull(); expect(favoriteIconAbsolutePath('user-1', '../etc/passwd', 'image/png')).toBeNull(); expect(favoriteIconAbsolutePath('a/b', 'fav-1', 'image/png')).toBeNull(); expect(favoriteIconAbsolutePath('a\\b', 'fav-1', 'image/png')).toBeNull(); expect(favoriteIconAbsolutePath('', 'fav-1', 'image/png')).toBeNull(); expect(favoriteIconAbsolutePath('user-1', '', 'image/png')).toBeNull(); }); }); describe('removeFavoriteIconFileBestEffort (quick-260923-lrr, T-LRR-07)', () => { let dir: string; const ORIGINAL_ENV = process.env.FAVORITE_ICONS_DIR; beforeAll(() => { dir = fs.mkdtempSync(path.join(os.tmpdir(), 'tessera-favorite-icons-rm-')); process.env.FAVORITE_ICONS_DIR = dir; }); afterAll(() => { fs.rmSync(dir, { recursive: true, force: true }); if (ORIGINAL_ENV === undefined) { delete process.env.FAVORITE_ICONS_DIR; } else { process.env.FAVORITE_ICONS_DIR = ORIGINAL_ENV; } }); afterEach(() => { fs.rmSync(path.join(dir, 'user-1'), { recursive: true, force: true }); }); it('entfernt eine vorhandene Datei und liefert true', async () => { const absolute = favoriteIconAbsolutePath('user-1', 'fav-1', 'image/png'); expect(absolute).not.toBeNull(); fs.mkdirSync(path.dirname(absolute as string), { recursive: true }); fs.writeFileSync(absolute as string, Buffer.from([1, 2, 3])); const result = await removeFavoriteIconFileBestEffort('user-1', 'fav-1', 'image/png'); expect(result).toBe(true); expect(fs.existsSync(absolute as string)).toBe(false); }); it('fehlende Datei -> false, wirft nicht', async () => { await expect(removeFavoriteIconFileBestEffort('user-1', 'fehlt', 'image/png')).resolves.toBe(false); }); it('unbekannter Typ -> false, wirft nicht', async () => { await expect( removeFavoriteIconFileBestEffort('user-1', 'fav-1', 'application/pdf'), ).resolves.toBe(false); }); }); describe('Grenzen (quick-260923-lrr)', () => { it('FAVORITE_ICON_MAX_BYTES ist 512 KiB', () => { expect(FAVORITE_ICON_MAX_BYTES).toBe(512 * 1024); }); });