fix(agrarmonitor): Lieferanten-Detailseite ohne AJAX-Header laden
Build and Push Multi-Platform Images / build-and-push (push) Successful in 38s
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:
1 parent
3d4125b29a
commit
549948913a
4 files changed
+82
-6
No files matched your search
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
baueFormData,
|
baueFormData,
|
||||||
|
beschreibeSeite,
|
||||||
bestimmeKandidaten,
|
bestimmeKandidaten,
|
||||||
hoechsteLieferantennummer,
|
hoechsteLieferantennummer,
|
||||||
lieferantennummerAusDetail,
|
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();
|
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 {
|
function istDisabled(el: Cheerio<Element>): boolean {
|
||||||
return el.attr('disabled') !== undefined;
|
return el.attr('disabled') !== undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ function fakeHttp(opts: {
|
|||||||
nummern: Record<number, string>;
|
nummern: Record<number, string>;
|
||||||
dubletten?: number[];
|
dubletten?: number[];
|
||||||
speichernIgnorieren?: number[];
|
speichernIgnorieren?: number[];
|
||||||
|
detailHtml?: string;
|
||||||
}) {
|
}) {
|
||||||
const aufrufe: string[] = [];
|
const aufrufe: string[] = [];
|
||||||
const updates: UpdateBody[] = [];
|
const updates: UpdateBody[] = [];
|
||||||
@@ -62,7 +63,8 @@ function fakeHttp(opts: {
|
|||||||
const m = /\/kunden\/detail\/(\d+)/.exec(url);
|
const m = /\/kunden\/detail\/(\d+)/.exec(url);
|
||||||
if (m) {
|
if (m) {
|
||||||
return Promise.resolve({
|
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: '' });
|
return Promise.resolve({ data: '' });
|
||||||
@@ -148,4 +150,23 @@ describe('LieferantennummernService.vergeben', () => {
|
|||||||
expect(lauf.eintraege[0].hinweis).toContain('15550706');
|
expect(lauf.eintraege[0].hinweis).toContain('15550706');
|
||||||
expect(aufrufe[aufrufe.length - 1]).toContain('itemsperpage=25');
|
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 {
|
import {
|
||||||
anzeigeName,
|
anzeigeName,
|
||||||
baueFormData,
|
baueFormData,
|
||||||
|
beschreibeSeite,
|
||||||
bestimmeKandidaten,
|
bestimmeKandidaten,
|
||||||
hoechsteLieferantennummer,
|
hoechsteLieferantennummer,
|
||||||
lieferantennummerAusDetail,
|
lieferantennummerAusDetail,
|
||||||
@@ -318,15 +319,29 @@ export class LieferantennummernService {
|
|||||||
return { maximum, kandidaten: bestimmeKandidaten(lieferanten) };
|
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(
|
private async ladeDetail(
|
||||||
http: AxiosInstance,
|
http: AxiosInstance,
|
||||||
kundeId: number,
|
kundeId: number,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const response = await http.get<string>(`/kunden/detail/${kundeId}`, {
|
const url = `/kunden/detail/${kundeId}`;
|
||||||
headers: AJAX_HEADERS,
|
const response = await http.get<string>(url, { responseType: 'text' });
|
||||||
responseType: 'text',
|
const html = String(response.data ?? '');
|
||||||
});
|
try {
|
||||||
return response.data;
|
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. */
|
/** Dublettencheck wie in der Weboberfläche; true, wenn Agrarmonitor ähnliche Kontakte meldet. */
|
||||||
|
|||||||
Reference in new issue
Block a user