From b92dd5dda198e2a2a9bf174767367ff4b7a2806e Mon Sep 17 00:00:00 2001 From: Schalli Date: Mon, 21 Sep 2026 13:32:31 +0200 Subject: [PATCH] fix(api,web): exec-Schleifen ohne Unterdrueckungskommentar umgeschrieben, LDAP-NUL-Maskierung und Sprachcookie-SameSite dokumentiert/gehaertet Drei while ((m = re.exec(t)) !== null)-Schleifen (dkv-parser.service.ts, dkv-parser.validate.ts, icon-discovery.service.ts) sind die korrekte Standardform fuer globale Regexe - kein verrutschtes "=". Umgeschrieben auf eine verhaltensgleiche for-Schleife, die ohne noAssignInExpressions- Unterdrueckung auskommt: Zuweisung wandert in Initialisierung und Fortschaltung der for-Schleife, Bedingung prueft weiterhin auf null. Abfolge der exec-Aufrufe, lastIndex-Fortschritt und Rumpfinhalte unveraendert. Neue Spezifikation dkv-parser.service.spec.ts deckt parseDkvText erstmals eigenstaendig ab (zwei Fahrzeugbloecke, Rechnungsnummer und -datum aus einer gemockten pdf-parse-Attrappe) - das Rueckfall-Tor fuer diesen Umbau. dkv-parser.validate.ts bleibt bei 27 Fahrzeugbloecken/66 Transaktionen gegen die reale invoice.pdf identisch. LdapService.escapeLdapFilterValue bleibt zeichengleich: der NUL-Treffer in der Regel ist die von RFC 4515 vorgeschriebene \00-Maskierung, kein Fehler. Ein biome-ignore-Kommentar dokumentiert das, statt die Funktion zu aendern. locale-switcher.tsx setzt jetzt SameSite=Lax auf dem NEXT_LOCALE-Cookie - path=/ und max-age waren bereits korrekt, es lag also kein Persistenzdefekt vor. Ohne SameSite haengt die Uebertragung am Browservorgabewert statt an einer Festlegung. Neue Spezifikation locale-switcher.test.tsx haelt die vollstaendige geschriebene Cookie-Zeichenkette fest. Biome-Warnungen 446 -> 434 (noControlCharactersInRegex/useIterableCallbackReturn/ noGlobalIsNan auf 0, noAssignInExpressions auf 4 und noDocumentCookie auf 17 - beide Reste ausschliesslich in Testdateien, suppressions/unused auf 0). Quick-Vorgang 260921-i8x, Task 3/3. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J --- apps/api/src/dkv/dkv-parser.service.spec.ts | 73 +++++++++++++++++++ apps/api/src/dkv/dkv-parser.service.ts | 7 +- apps/api/src/dkv/dkv-parser.validate.ts | 7 +- .../src/favorites/icon-discovery.service.ts | 3 +- apps/api/src/ldap/ldap.service.ts | 1 + .../src/components/locale-switcher.test.tsx | 61 ++++++++++++++++ apps/web/src/components/locale-switcher.tsx | 3 +- 7 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 apps/api/src/dkv/dkv-parser.service.spec.ts create mode 100644 apps/web/src/components/locale-switcher.test.tsx diff --git a/apps/api/src/dkv/dkv-parser.service.spec.ts b/apps/api/src/dkv/dkv-parser.service.spec.ts new file mode 100644 index 0000000..e98a889 --- /dev/null +++ b/apps/api/src/dkv/dkv-parser.service.spec.ts @@ -0,0 +1,73 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +/** + * DkvParserService.spec — Rueckfall-Tor fuer den exec-Schleifenumbau in + * parseDkvText (quick-260921-i8x, Task 3, T-i8x-04, RED first). + * + * `parseDkvText` hat heute keine eigene Abdeckung — `parsePdf` wird hier + * ueber eine gemockte `pdf-parse`-Attrappe (Muster: tender-mail.service.spec.ts + * fuer vi.mock auf ein Paket) mit einem festen DKV-Beispieltext mit zwei + * Fahrzeugbloecken gefuettert. Dieser Test haelt Kennzeichen, Kartennummern, + * Rechnungsnummer und Rechnungsdatum fest, bevor die `while ((match = ...))`- + * Schleife auf eine verhaltensgleiche `for`-Schleife umgeschrieben wird. + */ + +vi.mock('pdf-parse', () => ({ + PDFParse: vi.fn().mockImplementation(() => ({ + getText: vi.fn(async () => ({ text: SAMPLE_DKV_TEXT })), + destroy: vi.fn(async () => undefined), + })), +})); + +const SAMPLE_DKV_TEXT = `DKV Euro Service GmbH + Co. KG +Rechnung + +12/123456789/001 +15.04.2026 + +VEHICLE: GP-JL 728E CARD NO.: 1234567890123456 +01.04.2026\tESSO\tDONZDORF\t0071132\t189945\t68040 DIESEL\t0009 LTR\t45,910\t0\t0\t60,00\t50,00 + +VEHICLE: GP ML 720 CARD NO.: 9876543210987654 +02.04.2026\tARAL\tSTUTTGART\t0089221\t204983\t72150 DIESEL\t0011 LTR\t38,220\t0\t0\t55,00\t45,00 +`; + +describe('DkvParserService.parsePdf', () => { + let DkvParserService: typeof import('./dkv-parser.service').DkvParserService; + + beforeEach(async () => { + vi.clearAllMocks(); + ({ DkvParserService } = await import('./dkv-parser.service')); + }); + + it('liefert zwei Fahrzeugbloecke sowie Rechnungsnummer und -datum aus dem Beispieltext', async () => { + const service = new DkvParserService(); + + const result = await service.parsePdf(Buffer.from('irrelevant')); + + expect(result.vehicles).toHaveLength(2); + + expect(result.vehicles[0].kennzeichen).toBe('GP-JL 728E'); + expect(result.vehicles[0].cardNumber).toBe('1234567890123456'); + expect(result.vehicles[0].transactions).toHaveLength(1); + expect(result.vehicles[0].transactions[0]).toMatchObject({ + lieferdatum: '01.04.2026', + ort: 'DONZDORF', + produkt: 'DIESEL', + einheit: 'LTR', + }); + + expect(result.vehicles[1].kennzeichen).toBe('GP ML 720'); + expect(result.vehicles[1].cardNumber).toBe('9876543210987654'); + expect(result.vehicles[1].transactions).toHaveLength(1); + expect(result.vehicles[1].transactions[0]).toMatchObject({ + lieferdatum: '02.04.2026', + ort: 'STUTTGART', + produkt: 'DIESEL', + einheit: 'LTR', + }); + + expect(result.rechnungsnummer).toBe('12/123456789/001'); + expect(result.rechnungsdatum).toBe('15.04.2026'); + }); +}); diff --git a/apps/api/src/dkv/dkv-parser.service.ts b/apps/api/src/dkv/dkv-parser.service.ts index 46e8e48..d131c5f 100644 --- a/apps/api/src/dkv/dkv-parser.service.ts +++ b/apps/api/src/dkv/dkv-parser.service.ts @@ -100,8 +100,11 @@ export class DkvParserService { const vehicleBlockPattern = /VEHICLE:\s+([A-Z0-9 ._-]+?)\s+CARD NO\.:\s+(\S+)([\s\S]*?)(?=VEHICLE:|$)/g; - let match: RegExpExecArray | null; - while ((match = vehicleBlockPattern.exec(text)) !== null) { + for ( + let match = vehicleBlockPattern.exec(text); + match !== null; + match = vehicleBlockPattern.exec(text) + ) { const kennzeichen = match[1].trim(); const cardNumber = match[2].trim(); const blockText = match[3]; diff --git a/apps/api/src/dkv/dkv-parser.validate.ts b/apps/api/src/dkv/dkv-parser.validate.ts index a617d2f..b76f550 100644 --- a/apps/api/src/dkv/dkv-parser.validate.ts +++ b/apps/api/src/dkv/dkv-parser.validate.ts @@ -135,8 +135,11 @@ function parseDkvText(text: string): DkvVehicleBlock[] { const vehicleBlockPattern = /VEHICLE:\s+([A-Z0-9 ._-]+?)\s+CARD NO\.:\s+(\S+)([\s\S]*?)(?=VEHICLE:|$)/g; - let match: RegExpExecArray | null; - while ((match = vehicleBlockPattern.exec(text)) !== null) { + for ( + let match = vehicleBlockPattern.exec(text); + match !== null; + match = vehicleBlockPattern.exec(text) + ) { const kennzeichen = match[1].trim(); const cardNumber = match[2].trim(); const blockText = match[3]; diff --git a/apps/api/src/favorites/icon-discovery.service.ts b/apps/api/src/favorites/icon-discovery.service.ts index a6a7e32..91643c4 100644 --- a/apps/api/src/favorites/icon-discovery.service.ts +++ b/apps/api/src/favorites/icon-discovery.service.ts @@ -177,9 +177,8 @@ function getOriginFaviconUrl(pageUrl: string): string { function parseAttributes(tag: string): Record { const attrs: Record = {}; const re = /([a-zA-Z_:.-]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s"'>]+))/g; - let m: RegExpExecArray | null; - while ((m = re.exec(tag)) !== null) { + for (let m = re.exec(tag); m !== null; m = re.exec(tag)) { const key = m[1].toLowerCase(); const value = m[3] ?? m[4] ?? m[5] ?? ''; diff --git a/apps/api/src/ldap/ldap.service.ts b/apps/api/src/ldap/ldap.service.ts index 0142287..7623adf 100644 --- a/apps/api/src/ldap/ldap.service.ts +++ b/apps/api/src/ldap/ldap.service.ts @@ -1659,6 +1659,7 @@ export class LdapService { .replace(/\*/g, '\\2a') .replace(/\(/g, '\\28') .replace(/\)/g, '\\29') + // biome-ignore lint/suspicious/noControlCharactersInRegex: RFC 4515 verlangt genau dieses Steuerzeichen als \00-Maskierung; ohne sie kann ein unmaskiertes NUL den Filter beim Verzeichnisserver abschneiden (LDAP-Filter-Injection). .replace(/\x00/g, '\\00'); } diff --git a/apps/web/src/components/locale-switcher.test.tsx b/apps/web/src/components/locale-switcher.test.tsx new file mode 100644 index 0000000..bbcb021 --- /dev/null +++ b/apps/web/src/components/locale-switcher.test.tsx @@ -0,0 +1,61 @@ +import { cleanup, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { LocaleSwitcher } from './locale-switcher'; + +/** + * locale-switcher.test — SameSite-Luecke im Sprachumschalter-Cookie + * (quick-260921-i8x, Task 3 Teil C, T-i8x-05, RED first). + * + * Muster: header.test.tsx fuer next-intl/next-navigation-Mocks. Der Setter + * von `document.cookie` wird per `Object.defineProperty` durch eine + * Attrappe ersetzt, die jede geschriebene Zeichenkette sammelt — vor der + * Aenderung faellt nur die SameSite-Zusicherung durch, das ist beabsichtigt + * und zeigt genau die Luecke (T-i8x-05). + */ + +vi.mock('next-intl', () => ({ + useTranslations: () => (key: string) => { + const translations: Record = { + de: 'Deutsch', + en: 'English', + }; + return translations[key] ?? key; + }, + useLocale: () => 'de', +})); + +const refreshMock = vi.fn(); +vi.mock('next/navigation', () => ({ + useRouter: () => ({ refresh: refreshMock }), +})); + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); +}); + +describe('LocaleSwitcher', () => { + it('schreibt beim Klick genau ein Cookie mit NEXT_LOCALE, path=/, max-age=31536000 und SameSite=Lax', async () => { + const writtenCookies: string[] = []; + Object.defineProperty(document, 'cookie', { + configurable: true, + get: () => '', + set: (value: string) => { + writtenCookies.push(value); + }, + }); + + const user = userEvent.setup(); + render(); + + await user.click(screen.getByRole('button')); + + expect(writtenCookies).toHaveLength(1); + const written = writtenCookies[0]; + expect(written).toContain('NEXT_LOCALE=en'); + expect(written).toContain('path=/'); + expect(written).toContain('max-age=31536000'); + expect(written).toContain('SameSite=Lax'); + }); +}); diff --git a/apps/web/src/components/locale-switcher.tsx b/apps/web/src/components/locale-switcher.tsx index 628f37c..cde92c2 100644 --- a/apps/web/src/components/locale-switcher.tsx +++ b/apps/web/src/components/locale-switcher.tsx @@ -12,7 +12,8 @@ export function LocaleSwitcher() { function switchLocale() { const newLocale = locale === 'de' ? 'en' : 'de'; - document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000`; + // biome-ignore lint/suspicious/noDocumentCookie: die von Biome vorgeschlagene Cookie-Store-API fehlt in WebKit, die Sprachumschaltung waere damit im Tauri-Wrapper auf Linux und macOS wirkungslos. + document.cookie = `NEXT_LOCALE=${newLocale};path=/;max-age=31536000;SameSite=Lax`; startTransition(() => { router.refresh(); });