feat(api): Favoriten — Symbol trotz Zertifikatsfehler holen, Reihenfolge per PUT /favorites/order speichern
- icon-discovery.service.ts: undicis eigenes fetch mit Modul-Singleton
LENIENT_TLS_AGENT (Agent({ connect: { rejectUnauthorized: false } }))
als dispatcher in fetchWithRedirectGuard, der einzigen Ausgangsstelle
fuer HTML-Ermittlung und Icon-Byte-Holen; SSRF-Schutz unveraendert
- undici 7.28.0 (bereits im Lockfile aufgeloest) als direkte Abhaengigkeit
von @tessera/api via pnpm add --offline
- PUT /favorites/order (ReorderFavoritesDto) vor den :id-Routen;
FavoritesService.reorder() setzt position=index fuer die Favoriten
eines Widgets in EINER withTenantTransaction, userId+widgetId in jeder
Bedingung (zweites Netz), eine BadRequestException fuer alle
Abweichungen (T-JDD-06)
- getIcon: X-Content-Type-Options nosniff + restriktive CSP (T-JDD-02)
- 10 neue Tests (3 Dispatcher, 7 reorder); volle API-Suite 68 Dateien/
1101 Tests und type-check gruen
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,22 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
/**
|
||||
* 260917-jdd — `undici` wird gemockt, damit KEIN Test tatsaechlich ins Netz
|
||||
* geht: die produktive Datei ruft ab jetzt `undiciFetch` statt des globalen
|
||||
* `fetch` auf, mit einem Modul-Singleton-`Agent` als `dispatcher`. Die
|
||||
* Mock-Klasse zeichnet nur die uebergebenen `options` auf; `fetch` delegiert
|
||||
* ZUR LAUFZEIT (Pfeilfunktion, nicht beim Laden aufgeloest) an
|
||||
* `globalThis.fetch`, damit alle bestehenden `vi.stubGlobal('fetch', …)`-
|
||||
* Tests wortgleich gruen bleiben.
|
||||
*/
|
||||
vi.mock('undici', () => ({
|
||||
Agent: class Agent {
|
||||
constructor(public readonly options: unknown) {}
|
||||
},
|
||||
fetch: (...args: unknown[]) => (globalThis.fetch as any)(...args),
|
||||
}));
|
||||
|
||||
import { Agent } from 'undici';
|
||||
import {
|
||||
IconDiscoveryService,
|
||||
isPublicHttpUrl,
|
||||
@@ -203,3 +221,69 @@ describe('IconDiscoveryService.discoverFavoriteIconUrl (unchanged behaviour)', (
|
||||
expect(typeof result).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('IconDiscoveryService — Dispatcher (260917-jdd)', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('discoverFavoriteIconUrl uebergibt den tolerante-TLS-Agent als dispatcher und redirect: manual', async () => {
|
||||
const html = '<html><head><link rel="icon" href="https://ctl.de/fav.png" /></head></html>';
|
||||
const fetchSpy = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: {
|
||||
get: (n: string) =>
|
||||
n.toLowerCase() === 'content-type' ? 'text/html; charset=utf-8' : null,
|
||||
},
|
||||
text: async () => html,
|
||||
});
|
||||
vi.stubGlobal('fetch', fetchSpy);
|
||||
|
||||
const service = new IconDiscoveryService();
|
||||
await service.discoverFavoriteIconUrl('http://8.8.8.8');
|
||||
|
||||
const init = fetchSpy.mock.calls[0][1];
|
||||
expect(init.dispatcher).toBeInstanceOf(Agent);
|
||||
expect(init.dispatcher.options).toEqual({ connect: { rejectUnauthorized: false } });
|
||||
expect(init.redirect).toBe('manual');
|
||||
});
|
||||
|
||||
it('fetchIconBytes uebergibt denselben tolerante-TLS-Agent als dispatcher und redirect: manual', async () => {
|
||||
const fetchSpy = vi.fn().mockResolvedValue(mockResponse({ contentType: 'image/png' }));
|
||||
vi.stubGlobal('fetch', fetchSpy);
|
||||
|
||||
const service = new IconDiscoveryService();
|
||||
await service.fetchIconBytes('http://8.8.8.8/favicon.ico');
|
||||
|
||||
const init = fetchSpy.mock.calls[0][1];
|
||||
expect(init.dispatcher).toBeInstanceOf(Agent);
|
||||
expect(init.dispatcher.options).toEqual({ connect: { rejectUnauthorized: false } });
|
||||
expect(init.redirect).toBe('manual');
|
||||
});
|
||||
|
||||
it('Discovery und fetchIconBytes teilen DENSELBEN Agent (Modul-Singleton)', async () => {
|
||||
const html = '<html><head></head></html>';
|
||||
const fetchSpy = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
headers: {
|
||||
get: (n: string) =>
|
||||
n.toLowerCase() === 'content-type' ? 'text/html; charset=utf-8' : null,
|
||||
},
|
||||
text: async () => html,
|
||||
})
|
||||
.mockResolvedValueOnce(mockResponse({ contentType: 'image/png' }));
|
||||
vi.stubGlobal('fetch', fetchSpy);
|
||||
|
||||
const service = new IconDiscoveryService();
|
||||
await service.discoverFavoriteIconUrl('http://8.8.8.8');
|
||||
await service.fetchIconBytes('http://8.8.8.8/favicon.ico');
|
||||
|
||||
const calls = fetchSpy.mock.calls;
|
||||
expect(calls[0][1].dispatcher).toBe(calls[1][1].dispatcher);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user