fix(agrarmonitor): Lieferanten-Detailseite ohne AJAX-Header laden
Build and Push Multi-Platform Images / build-and-push (push) Successful in 38s

Im Einzeltest fehlte auf /kunden/detail/<id> das Feld
#lieferantennummer, obwohl die Liste kurz vorher mit derselben Session
geladen wurde. Die Detailseite wird jetzt wie eine normale
Browser-Navigation ohne X-Requested-With geladen (wie im Connector
bei den Eingangsrechnungen).

Fehlt das Feld trotzdem, nennt der Fehler HTTP-Status, Ziel-URL,
Seitentitel und Feldnamen – ohne Feldwerte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bjoernpoettkerandClaude Opus 5.5 committed 2026-09-28 09:34:49 +02:00
1 parent 3d4125b29a
commit 549948913a
4 files changed
+82 -6

No files matched your search

@@ -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 =
'<html><head><title>Anmeldung - AGRARMONITOR</title></head><body>' +
'<form><input name="email" value="geheim@example.de"><input name="iban" value="DE001234"></form></body></html>';
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');
});
});
@@ -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<Element>): boolean {
return el.attr('disabled') !== undefined;
}
@@ -53,6 +53,7 @@ function fakeHttp(opts: {
nummern: Record<number, string>;
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:
'<html><head><title>Anmeldung - AGRARMONITOR</title></head><body></body></html>',
});
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<string, string> }];
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');
});
});
@@ -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<string> {
const response = await http.get<string>(`/kunden/detail/${kundeId}`, {
headers: AJAX_HEADERS,
responseType: 'text',
});
return response.data;
const url = `/kunden/detail/${kundeId}`;
const response = await http.get<string>(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. */