From 7188c5b9585b46be3a68b2e4d3e96710bdfc99eb Mon Sep 17 00:00:00 2001 From: Schalli Date: Tue, 29 Sep 2026 15:32:30 +0200 Subject: [PATCH] fix(favorites): geaenderte Symbol-Adresse wird sofort angezeigt - eine neue, ausdruecklich eingetragene Logo-Adresse verdraengt ein frueher hochgeladenes Symbol (Vorrang der Datei liess die Adresse unsichtbar) - iconVersion steigt mit, die Kachel laedt das Bild neu - Regressionstests: Upload wird ersetzt, unveraenderte Adresse laesst Upload stehen Co-Authored-By: Claude Opus 5.5 (1M context) --- .../src/favorites/favorites.service.spec.ts | 34 +++++++++++++++++++ apps/api/src/favorites/favorites.service.ts | 24 +++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/apps/api/src/favorites/favorites.service.spec.ts b/apps/api/src/favorites/favorites.service.spec.ts index 476e116..b76ae2a 100644 --- a/apps/api/src/favorites/favorites.service.spec.ts +++ b/apps/api/src/favorites/favorites.service.spec.ts @@ -859,6 +859,40 @@ describe('FavoritesService — Bindung an forTenant() (260911-gwh)', () => { }); }); + describe('update — ausdrueckliche Logo-Adresse verdraengt ein hochgeladenes Symbol (260929-lh3)', () => { + const file = { buffer: PNG, originalname: 'x.png', mimetype: 'image/png', size: PNG.length }; + + it('neue, abweichende iconUrl bei vorhandenem Upload: Upload-Typ null, Datei weg, iconVersion erneut +1 — die neue Adresse wird angezeigt', async () => { + const prisma = makeFakePrisma([baseRow]); + const service = new FavoritesService(prisma as any, makeIconDiscovery() as any); + await service.uploadIcon('t1', 'f1', 'user-a1', file); + expect(fs.existsSync(fileFor('user-a1', 'f1', 'png'))).toBe(true); + + const updated = await service.update('t1', 'f1', 'user-a1', { + iconUrl: 'https://neu.invalid/logo.png', + } as any); + + expect(updated.iconUrl).toBe('https://neu.invalid/logo.png'); + expect(updated.uploadedIconMime).toBeNull(); + expect(updated.iconVersion).toBe(2); + expect(fs.existsSync(fileFor('user-a1', 'f1', 'png'))).toBe(false); + }); + + it('UNVERAENDERTE iconUrl bei vorhandenem Upload (das Formular schickt sie bei jedem Speichern mit): Upload bleibt', async () => { + const prisma = makeFakePrisma([baseRow]); + const service = new FavoritesService(prisma as any, makeIconDiscovery() as any); + await service.uploadIcon('t1', 'f1', 'user-a1', file); + + const updated = await service.update('t1', 'f1', 'user-a1', { + iconUrl: baseRow.iconUrl, + } as any); + + expect(updated.uploadedIconMime).toBe('image/png'); + expect(updated.iconVersion).toBe(1); + expect(fs.existsSync(fileFor('user-a1', 'f1', 'png'))).toBe(true); + }); + }); + describe('remove() mit hochgeladenem Symbol', () => { it('Zeile und Datei weg', async () => { const prisma = makeFakePrisma([baseRow]); diff --git a/apps/api/src/favorites/favorites.service.ts b/apps/api/src/favorites/favorites.service.ts index 3a51b22..50882ca 100644 --- a/apps/api/src/favorites/favorites.service.ts +++ b/apps/api/src/favorites/favorites.service.ts @@ -219,14 +219,34 @@ export class FavoritesService { } } - if (data.iconUrl !== undefined && data.iconUrl !== link.iconUrl) { + // 260929-lh3: eine NEUE, ausdruecklich eingetragene Logo-Adresse muss + // Vorrang vor einem frueher hochgeladenen Symbol haben. `getIconBytes` + // liefert bei gesetztem `uploadedIconMime` IMMER die Datei — ohne diesen + // Schritt blieb die neue Adresse gespeichert, aber unsichtbar (die Kachel + // zeigte weiter das alte hochgeladene Bild). Nur bei einer tatsaechlichen + // Aenderung: das Formular schickt die unveraenderte Adresse bei jedem + // Speichern mit, das darf ein hochgeladenes Symbol nicht verdraengen. + const iconUrlChanged = data.iconUrl !== undefined && data.iconUrl !== link.iconUrl; + const explicitUrlReplacesUpload = + iconUrlChanged && Boolean(dto.iconUrl) && link.uploadedIconMime !== null; + if (explicitUrlReplacesUpload) { + data.uploadedIconMime = null; + } + + if (iconUrlChanged) { data.iconVersion = { increment: 1 }; } - return tenantPrisma.favoriteLink.update({ + const updated = await tenantPrisma.favoriteLink.update({ where: { id }, data, }); + + if (explicitUrlReplacesUpload && link.uploadedIconMime !== null) { + await this.removeIconFile(id, link.userId, link.uploadedIconMime, 'ersetzte'); + } + + return updated; } /**