diff --git a/paperless-backend/src/agrarmonitor/lieferantennummern.parser.spec.ts b/paperless-backend/src/agrarmonitor/lieferantennummern.parser.spec.ts index ebb434b..923ccd3 100644 --- a/paperless-backend/src/agrarmonitor/lieferantennummern.parser.spec.ts +++ b/paperless-backend/src/agrarmonitor/lieferantennummern.parser.spec.ts @@ -1,5 +1,6 @@ import { baueFormData, + beschreibeSeite, bestimmeKandidaten, hoechsteLieferantennummer, lieferantennummerAusDetail, @@ -194,3 +195,17 @@ describe('lieferantennummerAusDetail', () => { ); }); }); + +describe('beschreibeSeite', () => { + it('nennt Titel und Feldnamen, aber keine Werte', () => { + const html = + 'Anmeldung - AGRARMONITOR' + + '
'; + const text = beschreibeSeite(html); + expect(text).toContain('Titel „Anmeldung - AGRARMONITOR“'); + expect(text).toContain('#form_detail fehlt'); + expect(text).toContain('Felder: email, iban'); + expect(text).not.toContain('geheim@example.de'); + expect(text).not.toContain('DE001234'); + }); +}); diff --git a/paperless-backend/src/agrarmonitor/lieferantennummern.parser.ts b/paperless-backend/src/agrarmonitor/lieferantennummern.parser.ts index 9be3ee9..ac64f1b 100644 --- a/paperless-backend/src/agrarmonitor/lieferantennummern.parser.ts +++ b/paperless-backend/src/agrarmonitor/lieferantennummern.parser.ts @@ -90,6 +90,31 @@ export function lieferantennummerAusDetail(html: string): string { return (input.attr('value') ?? '').trim(); } +/** + * Kurzbeschreibung einer unerwarteten Seite für das Log: Titel, Formular und + * Feldnamen – bewusst ohne Feldwerte (IBAN o. Ä.). + */ +export function beschreibeSeite(html: string): string { + const $ = cheerio.load(html); + const titel = $('title').first().text().trim() || '–'; + const form = $('form#form_detail'); + const namen = Array.from( + new Set( + $('input[name], select[name], textarea[name]') + .toArray() + .map((el) => $(el).attr('name')!), + ), + ); + const feldListe = + namen.length > 15 + ? `${namen.slice(0, 15).join(', ')}, …` + : namen.join(', '); + return ( + `Titel „${titel}“, #form_detail ${form.length ? 'vorhanden' : 'fehlt'}, ` + + `${html.length} Zeichen, Felder: ${feldListe || 'keine'}` + ); +} + function istDisabled(el: Cheerio): boolean { return el.attr('disabled') !== undefined; } diff --git a/paperless-backend/src/agrarmonitor/lieferantennummern.service.spec.ts b/paperless-backend/src/agrarmonitor/lieferantennummern.service.spec.ts index 43d381a..c07d8e4 100644 --- a/paperless-backend/src/agrarmonitor/lieferantennummern.service.spec.ts +++ b/paperless-backend/src/agrarmonitor/lieferantennummern.service.spec.ts @@ -53,6 +53,7 @@ function fakeHttp(opts: { nummern: Record; dubletten?: number[]; speichernIgnorieren?: number[]; + detailHtml?: string; }) { const aufrufe: string[] = []; const updates: UpdateBody[] = []; @@ -62,7 +63,8 @@ function fakeHttp(opts: { const m = /\/kunden\/detail\/(\d+)/.exec(url); if (m) { return Promise.resolve({ - data: detail(opts.nummern[Number(m[1])] ?? ''), + status: 200, + data: opts.detailHtml ?? detail(opts.nummern[Number(m[1])] ?? ''), }); } return Promise.resolve({ data: '' }); @@ -148,4 +150,23 @@ describe('LieferantennummernService.vergeben', () => { expect(lauf.eintraege[0].hinweis).toContain('15550706'); expect(aufrufe[aufrufe.length - 1]).toContain('itemsperpage=25'); }); + + it('lädt die Detailseite ohne AJAX-Header und meldet eine unerwartete Seite verständlich', async () => { + const { http, updates } = fakeHttp({ + nummern: {}, + detailHtml: + 'Anmeldung - AGRARMONITOR', + }); + const lauf = neuerLauf(1); + await service().vergeben(http, [kandidat(1, 'A')], 15550706, lauf); + + const detailAufruf = (http.get as jest.Mock).mock.calls.find( + ([url]) => url === '/kunden/detail/1', + ) as [string, { headers?: Record }]; + expect(detailAufruf[1].headers?.['X-Requested-With']).toBeUndefined(); + expect(updates).toHaveLength(0); + expect(lauf).toMatchObject({ fehler: 1, abgebrochen: true }); + expect(lauf.eintraege[0].hinweis).toContain('HTTP 200'); + expect(lauf.eintraege[0].hinweis).toContain('Anmeldung - AGRARMONITOR'); + }); }); diff --git a/paperless-backend/src/agrarmonitor/lieferantennummern.service.ts b/paperless-backend/src/agrarmonitor/lieferantennummern.service.ts index 9c165b7..a59fb5d 100644 --- a/paperless-backend/src/agrarmonitor/lieferantennummern.service.ts +++ b/paperless-backend/src/agrarmonitor/lieferantennummern.service.ts @@ -10,6 +10,7 @@ import { fehlerText } from './agrarmonitor-polling.service'; import { anzeigeName, baueFormData, + beschreibeSeite, bestimmeKandidaten, hoechsteLieferantennummer, lieferantennummerAusDetail, @@ -318,15 +319,29 @@ export class LieferantennummernService { return { maximum, kandidaten: bestimmeKandidaten(lieferanten) }; } + /** + * Lädt die Detailseite wie eine normale Browser-Navigation (ohne AJAX-Header) + * und prüft, dass das Formular mit #lieferantennummer enthalten ist. + */ private async ladeDetail( http: AxiosInstance, kundeId: number, ): Promise { - const response = await http.get(`/kunden/detail/${kundeId}`, { - headers: AJAX_HEADERS, - responseType: 'text', - }); - return response.data; + const url = `/kunden/detail/${kundeId}`; + const response = await http.get(url, { responseType: 'text' }); + const html = String(response.data ?? ''); + try { + lieferantennummerAusDetail(html); + } catch { + const request = response.request as + | { res?: { responseUrl?: string } } + | undefined; + const zielUrl = request?.res?.responseUrl ?? url; + throw new Error( + `Detailseite ohne Feld #lieferantennummer (HTTP ${response.status}, ${zielUrl}): ${beschreibeSeite(html)}`, + ); + } + return html; } /** Dublettencheck wie in der Weboberfläche; true, wenn Agrarmonitor ähnliche Kontakte meldet. */