spa(task 4 polish): decodeURIComponent crash-safe, edge-case-tests

- 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) <noreply@anthropic.com>
This commit is contained in:
Jörg Lohrer 2026-04-15 15:17:38 +02:00
parent 47decd9b70
commit 36dd76a88f
2 changed files with 25 additions and 1 deletions

View File

@ -1,11 +1,19 @@
/** /**
* Erkennt Legacy-Hugo-URLs der Form /YYYY/MM/DD/<dtag>.html oder .../<dtag>.html/ * Erkennt Legacy-Hugo-URLs der Form /YYYY/MM/DD/<dtag>.html oder .../<dtag>.html/
* und gibt den dtag-Teil zurück. Für alle anderen Pfade: null. * 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 { export function parseLegacyUrl(path: string): string | null {
const match = path.match(/^\/\d{4}\/\d{2}\/\d{2}\/([^/]+?)\.html\/?$/); const match = path.match(/^\/\d{4}\/\d{2}\/\d{2}\/([^/]+?)\.html\/?$/);
if (!match) return null; if (!match) return null;
try {
return decodeURIComponent(match[1]); return decodeURIComponent(match[1]);
} catch {
return null;
}
} }
/** /**

View File

@ -32,6 +32,14 @@ describe('parseLegacyUrl', () => {
'mit leerzeichen', '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', () => { describe('canonicalPostPath', () => {
@ -43,3 +51,11 @@ describe('canonicalPostPath', () => {
expect(canonicalPostPath('mit leerzeichen')).toBe('/mit%20leerzeichen/'); 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/');
});
});