From 36dd76a88f68424ea82fc7e82997b5e69693f8e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Lohrer?= Date: Wed, 15 Apr 2026 15:17:38 +0200 Subject: [PATCH] spa(task 4 polish): decodeURIComponent crash-safe, edge-case-tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - decodeURIComponent in try/catch (malformed URI encoding crasht den SPA-Boot-Path nicht mehr, returned stattdessen null). - JSDoc präzisiert: erwartet nur Pfad ohne Query/Fragment. - Neue Tests: malformed %E0 → null, leerer dtag → null, round-trip Legacy → canonical. Co-Authored-By: Claude Opus 4.6 (1M context) --- app/src/lib/url/legacy.ts | 10 +++++++++- app/tests/unit/legacy-url.test.ts | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/app/src/lib/url/legacy.ts b/app/src/lib/url/legacy.ts index 5e3cbd3..8ffdbee 100644 --- a/app/src/lib/url/legacy.ts +++ b/app/src/lib/url/legacy.ts @@ -1,11 +1,19 @@ /** * Erkennt Legacy-Hugo-URLs der Form /YYYY/MM/DD/.html oder .../.html/ * und gibt den dtag-Teil zurück. Für alle anderen Pfade: null. + * + * Erwartet nur den Pfad ohne Query/Fragment — wenn vorhanden vom Aufrufer + * trennen. `decodeURIComponent` wird defensiv gekapselt, damit malformed + * Percent-Encoding die SPA beim Boot nicht crasht. */ export function parseLegacyUrl(path: string): string | null { const match = path.match(/^\/\d{4}\/\d{2}\/\d{2}\/([^/]+?)\.html\/?$/); if (!match) return null; - return decodeURIComponent(match[1]); + try { + return decodeURIComponent(match[1]); + } catch { + return null; + } } /** diff --git a/app/tests/unit/legacy-url.test.ts b/app/tests/unit/legacy-url.test.ts index a1fd8b3..b3f5ef8 100644 --- a/app/tests/unit/legacy-url.test.ts +++ b/app/tests/unit/legacy-url.test.ts @@ -32,6 +32,14 @@ describe('parseLegacyUrl', () => { 'mit leerzeichen', ); }); + + it('gibt null zurück bei malformed percent-encoding (crash-sicher)', () => { + expect(parseLegacyUrl('/2024/01/26/%E0.html/')).toBeNull(); + }); + + it('gibt null zurück für leeren dtag', () => { + expect(parseLegacyUrl('/2024/01/26/.html/')).toBeNull(); + }); }); describe('canonicalPostPath', () => { @@ -43,3 +51,11 @@ describe('canonicalPostPath', () => { expect(canonicalPostPath('mit leerzeichen')).toBe('/mit%20leerzeichen/'); }); }); + +describe('round-trip parseLegacyUrl → canonicalPostPath', () => { + it('Legacy-URL wird zur kanonischen kurzen Form', () => { + const dtag = parseLegacyUrl('/2025/03/04/dezentrale-oep-oer.html/'); + expect(dtag).not.toBeNull(); + expect(canonicalPostPath(dtag!)).toBe('/dezentrale-oep-oer/'); + }); +});