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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TPPB4ApQxzSU1rwV2Ffj9J
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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];
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -177,9 +177,8 @@ function getOriginFaviconUrl(pageUrl: string): string {
|
||||
function parseAttributes(tag: string): Record<string, string> {
|
||||
const attrs: Record<string, string> = {};
|
||||
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] ?? '';
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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<string, string> = {
|
||||
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(<LocaleSwitcher />);
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user