feat(quick-260921-pi9): Bilderrahmen-API - Bilder je Benutzer in der Datenbank, Magic-Byte-Pruefung, 5 MiB / 30 Stueck
- Prisma-Modell DashboardImage (bytea) mit Migration 20260921120000: Tabelle, Indizes, RLS ENABLE/FORCE und tenant_isolation_policy mit Benutzerdimension - dashboard-image-rules.ts: detectImageMime ueber Magic Bytes (PNG/JPEG/GIF/ WebP), Grenzen 5 MiB je Datei und 30 je Benutzer - DashboardImagesService: list/upload/getBytes/remove, je Methode forTenant(prisma, tenantId, userId); Besitz = Mandant UND Benutzer, sonst 404 - DashboardImagesController unter dashboard/images: GET, POST (FileInterceptor image, 5 MiB, eine Datei), GET :id mit Content-Type aus dem erkannten Typ, Cache-Control private, nosniff, Content-Disposition inline ohne Dateinamen, CSP sandbox; DELETE :id - CreateWidgetDto kennt 'picture-frame' - Klassifikationsdokument: neues Paar dashboard-images.service.ts/ dashboardImage; Bereichs- und Summenzeilen nachgemessen (dashboard 12->18, settings 3->4 und bug-reports waren in der Summe nie mitgezaehlt) - Befund: Prisma-Bytes verlangt Uint8Array<ArrayBuffer>, multers Buffer wird ohne Zusicherung abgelehnt - Kopie per new Uint8Array(buffer) statt Cast Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
DASHBOARD_IMAGE_MAX_BYTES,
|
||||
DASHBOARD_IMAGE_MAX_COUNT,
|
||||
detectImageMime,
|
||||
} from './dashboard-image-rules';
|
||||
|
||||
/**
|
||||
* dashboard-image-rules.spec — NEU (quick-260921-pi9, Bilderrahmen-Widget).
|
||||
*
|
||||
* Die Magic-Byte-Erkennung entscheidet Annahme UND gespeicherten Typ eines
|
||||
* hochgeladenen Bildes (T-PI9-01); Dateiname und behaupteter MIME-Typ werden
|
||||
* nie gelesen. Deshalb wird sie hier direkt an den Bytes geprueft: vier
|
||||
* gueltige Signaturen, und alles, was NICHT durchgehen darf (leer, Text,
|
||||
* SVG, PDF, RIFF ohne WEBP).
|
||||
*/
|
||||
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('detectImageMime (quick-260921-pi9)', () => {
|
||||
it('Test 1: PNG-Signatur 89 50 4E 47 0D 0A 1A 0A -> image/png', () => {
|
||||
expect(detectImageMime(bytes([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], [0, 0, 0, 13]))).toBe(
|
||||
'image/png',
|
||||
);
|
||||
});
|
||||
|
||||
it('Test 2: FF D8 FF -> image/jpeg', () => {
|
||||
expect(detectImageMime(bytes([0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10], 'JFIF'))).toBe('image/jpeg');
|
||||
});
|
||||
|
||||
it('Test 3: GIF87a und GIF89a -> image/gif', () => {
|
||||
expect(detectImageMime(bytes('GIF87a', [1, 0, 1, 0]))).toBe('image/gif');
|
||||
expect(detectImageMime(bytes('GIF89a', [1, 0, 1, 0]))).toBe('image/gif');
|
||||
});
|
||||
|
||||
it('Test 4: RIFF????WEBP -> image/webp', () => {
|
||||
expect(detectImageMime(bytes('RIFF', [0x24, 0x00, 0x00, 0x00], 'WEBP', 'VP8 '))).toBe('image/webp');
|
||||
});
|
||||
|
||||
it('Test 5: leerer Puffer -> null', () => {
|
||||
expect(detectImageMime(new Uint8Array(0))).toBeNull();
|
||||
});
|
||||
|
||||
it('Test 6: Textdatei -> null, auch wenn sie lang genug ist', () => {
|
||||
expect(detectImageMime(bytes('Dies ist keine Bilddatei, sondern Text.'))).toBeNull();
|
||||
});
|
||||
|
||||
it('Test 7: SVG-Text und PDF (%PDF) -> null', () => {
|
||||
expect(detectImageMime(bytes('<svg xmlns="http://www.w3.org/2000/svg"></svg>'))).toBeNull();
|
||||
expect(detectImageMime(bytes('%PDF-1.7\n%\xe2\xe3'))).toBeNull();
|
||||
});
|
||||
|
||||
it('Test 8: RIFF-Kopf ohne WEBP an Stelle 8 (z. B. WAVE) -> null', () => {
|
||||
expect(detectImageMime(bytes('RIFF', [0x24, 0x00, 0x00, 0x00], 'WAVE', 'fmt '))).toBeNull();
|
||||
});
|
||||
|
||||
it('Test 9: zu kurze Puffer werfen nicht — PNG-Anfang mit 4 Bytes ist null, JPEG-Anfang mit 2 Bytes ist null', () => {
|
||||
expect(detectImageMime(bytes([0x89, 0x50, 0x4e, 0x47]))).toBeNull();
|
||||
expect(detectImageMime(bytes([0xff, 0xd8]))).toBeNull();
|
||||
expect(detectImageMime(bytes('RIFF'))).toBeNull();
|
||||
});
|
||||
|
||||
it('Test 10: Grenzen — 5 MiB je Datei, 30 Bilder je Benutzer', () => {
|
||||
expect(DASHBOARD_IMAGE_MAX_BYTES).toBe(5 * 1024 * 1024);
|
||||
expect(DASHBOARD_IMAGE_MAX_COUNT).toBe(30);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user