fix(favorites): Symbol-Adresse auch speichern, wenn nur der Browser sie laden kann
- API: ausdrueckliche iconUrl wird nur auf Form (http/https, <= 2048) geprueft und auch gespeichert, wenn der Server sie nicht abrufen kann; keine 422 mehr - Erkennung: Seite mit Fehlerstatus, aber HTML mit <link rel=icon>, liefert diesen Verweis (docuvita); og:image einer Fehlerseite zaehlt nicht - Kachel: Proxy -> iconUrl direkt im Browser (no-referrer, nur http/https) -> Origin-Favicon -> Buchstabe - Meldung iconUrlUnreachable (de/en) entfernt, Hinweis zum Vorrang angepasst Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,6 @@ import {
|
||||
HttpException,
|
||||
NotFoundException,
|
||||
PayloadTooLargeException,
|
||||
UnprocessableEntityException,
|
||||
} from '@nestjs/common';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { FavoritesService } from './favorites.service';
|
||||
@@ -920,26 +919,50 @@ describe('FavoritesService — Bindung an forTenant() (260911-gwh)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('create/update — Abrufprobe fuer eine explizite iconUrl (260923-lrr)', () => {
|
||||
it('create mit expliziter iconUrl: Probe genau einmal; wirft -> UnprocessableEntityException, favoriteLink.create NICHT aufgerufen', async () => {
|
||||
const prisma = makeFakePrisma([], [{ id: 'widget-a1', userId: 'user-a1', tenantId: 't1' }]);
|
||||
const iconDiscovery = makeIconDiscovery({
|
||||
fetchIconBytes: vi.fn(async () => {
|
||||
throw new Error('blocked');
|
||||
}),
|
||||
describe('create/update — ausdrueckliche iconUrl: nur Formpruefung, kein Abruf (260929-lh3)', () => {
|
||||
const widgets = [{ id: 'widget-a1', userId: 'user-a1', tenantId: 't1' }];
|
||||
const failingFetch = () =>
|
||||
vi.fn(async () => {
|
||||
throw new Error('server bekommt 404/HTML');
|
||||
});
|
||||
|
||||
it('create mit einer Adresse, die der SERVER nicht abrufen kann: wird gespeichert, KEIN Abruf, KEINE Erkennung', async () => {
|
||||
const prisma = makeFakePrisma([], widgets);
|
||||
const iconDiscovery = makeIconDiscovery({ fetchIconBytes: failingFetch() });
|
||||
const service = new FavoritesService(prisma as any, iconDiscovery as any);
|
||||
|
||||
const created = await service.create('t1', 'user-a1', {
|
||||
widgetId: 'widget-a1',
|
||||
title: 'Docuvita',
|
||||
url: 'https://docuvita.ctl.local/server/services/web/',
|
||||
iconUrl: 'https://docuvita.ctl.local/webclient/docuvita/resources/brandimage/favicon.ico',
|
||||
} as any);
|
||||
|
||||
expect(created.iconUrl).toBe(
|
||||
'https://docuvita.ctl.local/webclient/docuvita/resources/brandimage/favicon.ico',
|
||||
);
|
||||
expect(iconDiscovery.fetchIconBytes).not.toHaveBeenCalled();
|
||||
expect(iconDiscovery.discoverFavoriteIconUrl).not.toHaveBeenCalled();
|
||||
expect(prisma.__favorites.size).toBe(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['kein http/https', 'ftp://x.invalid/icon.png'],
|
||||
['javascript-Schema', 'javascript:alert(1)'],
|
||||
['keine Adresse', 'kein url'],
|
||||
['laenger als 2048 Zeichen', `https://x.invalid/${'a'.repeat(2050)}`],
|
||||
])('create mit ungueltiger iconUrl (%s) -> BadRequestException, nichts geschrieben', async (_label, iconUrl) => {
|
||||
const prisma = makeFakePrisma([], widgets);
|
||||
const service = new FavoritesService(prisma as any, makeIconDiscovery() as any);
|
||||
|
||||
await expect(
|
||||
service.create('t1', 'user-a1', {
|
||||
widgetId: 'widget-a1',
|
||||
title: 'X',
|
||||
url: 'https://x.invalid',
|
||||
iconUrl: 'https://x.invalid/logo.png',
|
||||
iconUrl,
|
||||
} as any),
|
||||
).rejects.toThrow(UnprocessableEntityException);
|
||||
expect(iconDiscovery.fetchIconBytes).toHaveBeenCalledTimes(1);
|
||||
expect(iconDiscovery.fetchIconBytes).toHaveBeenCalledWith('https://x.invalid/logo.png');
|
||||
).rejects.toThrow(BadRequestException);
|
||||
expect(prisma.__favorites.size).toBe(0);
|
||||
});
|
||||
|
||||
@@ -956,24 +979,31 @@ describe('FavoritesService — Bindung an forTenant() (260911-gwh)', () => {
|
||||
iconVersion: 0,
|
||||
};
|
||||
|
||||
it('update mit neuer, abweichender iconUrl: fetchIconBytes genau einmal mit dieser Adresse; wirft -> UnprocessableEntityException, favoriteLink.update NICHT aufgerufen', async () => {
|
||||
it('update mit neuer iconUrl, die der Server nicht abrufen kann: gespeichert, iconVersion +1, KEIN Abruf', async () => {
|
||||
const prisma = makeFakePrisma([baseRow]);
|
||||
const iconDiscovery = makeIconDiscovery({
|
||||
fetchIconBytes: vi.fn(async () => {
|
||||
throw new Error('blocked');
|
||||
}),
|
||||
});
|
||||
const iconDiscovery = makeIconDiscovery({ fetchIconBytes: failingFetch() });
|
||||
const service = new FavoritesService(prisma as any, iconDiscovery as any);
|
||||
|
||||
const updated = await service.update('t1', 'f1', 'user-a1', {
|
||||
iconUrl: 'https://neu.invalid/icon.png',
|
||||
} as any);
|
||||
|
||||
expect(updated.iconUrl).toBe('https://neu.invalid/icon.png');
|
||||
expect(updated.iconVersion).toBe(1);
|
||||
expect(iconDiscovery.fetchIconBytes).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('update mit ungueltiger neuer iconUrl -> BadRequestException, Zeile unveraendert', async () => {
|
||||
const prisma = makeFakePrisma([baseRow]);
|
||||
const service = new FavoritesService(prisma as any, makeIconDiscovery() as any);
|
||||
|
||||
await expect(
|
||||
service.update('t1', 'f1', 'user-a1', { iconUrl: 'https://neu.invalid/icon.png' } as any),
|
||||
).rejects.toThrow(UnprocessableEntityException);
|
||||
expect(iconDiscovery.fetchIconBytes).toHaveBeenCalledTimes(1);
|
||||
expect(iconDiscovery.fetchIconBytes).toHaveBeenCalledWith('https://neu.invalid/icon.png');
|
||||
service.update('t1', 'f1', 'user-a1', { iconUrl: 'file:///etc/passwd' } as any),
|
||||
).rejects.toThrow(BadRequestException);
|
||||
expect(prisma.__favorites.get('f1').iconUrl).toBe(baseRow.iconUrl);
|
||||
});
|
||||
|
||||
it('update mit UNVERAENDERTER iconUrl: keine Probe, keine Erhoehung', async () => {
|
||||
it('update mit UNVERAENDERTER iconUrl: keine Pruefung, keine Erhoehung', async () => {
|
||||
const prisma = makeFakePrisma([baseRow]);
|
||||
const iconDiscovery = makeIconDiscovery();
|
||||
const service = new FavoritesService(prisma as any, iconDiscovery as any);
|
||||
@@ -992,18 +1022,5 @@ describe('FavoritesService — Bindung an forTenant() (260911-gwh)', () => {
|
||||
|
||||
expect(updated.iconVersion).toBe(0);
|
||||
});
|
||||
|
||||
it('update mit neuer, erreichbarer iconUrl: iconVersion +1', async () => {
|
||||
const prisma = makeFakePrisma([baseRow]);
|
||||
const iconDiscovery = makeIconDiscovery();
|
||||
const service = new FavoritesService(prisma as any, iconDiscovery as any);
|
||||
|
||||
const updated = await service.update('t1', 'f1', 'user-a1', {
|
||||
iconUrl: 'https://neu.invalid/icon.png',
|
||||
} as any);
|
||||
|
||||
expect(iconDiscovery.fetchIconBytes).toHaveBeenCalledWith('https://neu.invalid/icon.png');
|
||||
expect(updated.iconVersion).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user