Files
votianng/legacy-ui-tests/support/network.ts
T
Sven 9de2358480 Portiere PHP-Cronjobs auf Spring-Tasks und ergänze Legacy-Tests
Migriert die weiterhin benötigten Zeitaufgaben einschließlich SOAP-Importen, Betriebsstatus und Wiederanlaufkontrolle. Aktiviert 115 native Spring-Tasks im Docker-Image ohne Cron-Daemon oder PHP und dokumentiert die entfallenen HHA-Aufrufe. Ergänzt PHP-/UI-Testwerkzeuge und aktualisiert den Projektstand auf Version 0.2.0.
2026-10-27 09:53:23 +01:00

145 lines
9.3 KiB
TypeScript

import type { BrowserContext } from '@playwright/test';
import { appendFileSync, mkdirSync, existsSync, readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
const ledgerDirectory = fileURLToPath(new URL('../reports/', import.meta.url));
export function recordOrder(event: Record<string, unknown>) {
mkdirSync(ledgerDirectory, { recursive: true });
appendFileSync(ledgerDirectory + 'created-orders.jsonl', JSON.stringify({ time: new Date().toISOString(), ...event }) + '\n', { mode: 0o600 });
}
// The old form uses ISO-8859-1, including superscript station delimiters.
function formData(body: string) {
const utf8 = new URLSearchParams(body);
if (![...utf8.values()].some(value => value.includes('\uFFFD'))) return utf8;
const decode = (value: string) => new TextDecoder('windows-1252').decode(Buffer.from(value.replace(/\+/g, ' ').replace(/%([0-9a-f]{2})/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16))), 'latin1'));
return new URLSearchParams(body.split('&').map(pair => { const index = pair.indexOf('='); return [decode(pair.slice(0, index)), decode(pair.slice(index + 1))]; }));
}
function validateCreation(data: URLSearchParams, marker: string): string[] {
const problems: string[] = [];
const required = { toursubmit: 'Senden', getPriceOnly: '', jb_id: '', csc_id_payer: '878914', jb_orderer_hidden: marker, jb_status_manual: '1', jb_incomplete: '1', jb_multi_factor: '1', sendmail: '0', jb_fixprice: '10,00' };
for (const [key, expected] of Object.entries(required)) {
if ((data.get(key) ?? '') !== expected) problems.push(key);
}
for (const key of ['cr_id_order', 'cr_id_order_hidden', 'cr_id_orders', 'jb_permanent', 'jb_multi', 'jb_globaljob', 'afterwards', 'jb_offer', 'jb_insurance', 'jb_id_parent', 'jb_id_org', 'jb_tourname', 'jb_tourname_hidden', 'tmp_add_jb_ids', 'tmp_add_sids', 'do_delete_suborder', 'jb_offer_mail_hidden', 'jb_cashpayer_mail_hidden']) {
if (!['', '0'].includes(data.get(key) ?? '')) problems.push(key);
}
if (!data.get('request_token')) problems.push('request_token missing');
const stations = (data.get('jobList') ?? '').split('¹').map(row => row.split('²')).filter(row => [0, 1, 3, 4, 6, 7, 8].some(index => row[index]));
if (stations.length !== 2 || stations[0][0] !== '878914' || stations[1][0] !== '878915' || !stations.every(row => row.some(cell => cell.includes(marker)))) problems.push('exactly two marked test stations');
return problems;
}
const origin = 'https://test.sb.assecutor.de';
const pages = new Set([
'/', '/index.php', '/admin/login.php', '/admin/start.php',
'/jobs/job_edit.php', '/jobs/job_header.php', '/jobs/job_tour.php',
'/jobs/job_options.php', '/jobs/job_tour_list.php',
'/admin/cs_csc_list.php',
'/css/phoenix.css.php', '/css/navigation.css.php',
'/include/maps.googleapis.com_maps_api_js_places.php',
]);
export async function installGuard(context: BrowserContext, login = false) {
const denied: string[] = [];
const allowed: string[] = [];
const errors: string[] = [];
const knownErrors: string[] = [];
const suppressed: string[] = [];
const consoleErrors: string[] = [];
let creationMarker: string | undefined;
let saveAttempts = 0;
const readableOrders = new Set<string>();
context.on('page', page => {
page.on('console', message => { if (['error', 'warning'].includes(message.type())) consoleErrors.push(message.text()); });
page.on('pageerror', error => {
// Still fails the dedicated regression test; do not mask unrelated exceptions.
if (["Cannot read properties of null (reading 'top')", 'can\'t access property "top", top.opener is null'].includes(error.message) && /\/jobs\/job_edit\.php[^\n]*:13:\d+/.test(error.stack ?? '')) knownErrors.push(error.stack!);
else errors.push(error.stack ?? error.message);
});
});
await context.route('**/*', async route => {
const request = route.request();
const url = new URL(request.url());
const method = request.method();
const description = `${method} ${url.origin}${url.pathname} [${[...url.searchParams.keys()].join(',')}]`;
if (url.origin === origin && url.pathname === '/include/ajaxReqLib.php' &&
url.searchParams.get('mode') === '500' && url.searchParams.get('value_03') === 'usr_state_newsticker') {
suppressed.push('News ticker acknowledgement');
return route.fulfill({ status: 200, contentType: 'application/javascript', body: "retValue = '1';" });
}
if (url.pathname === '/include/ajaxJsError.php' && url.origin === origin) {
errors.push(url.searchParams.get('message') ?? 'Legacy JavaScript error');
return route.fulfill({ status: 204 });
}
if (url.hostname === 'maps.googleapis.com' && url.pathname.endsWith('/gen_204')) {
suppressed.push('Google Maps telemetry');
return route.fulfill({ status: 204 });
}
let ok = false;
if (url.origin === origin) {
if (method === 'GET') {
ok = /\.(?:js|css|png|gif|jpe?g|svg|ico|woff2?|ttf)$/i.test(url.pathname) || pages.has(url.pathname);
// Never permit action parameters on otherwise readable pages.
for (const key of ['f_act', 'action', 'save', 'delete', 'sendmail', 'toursubmit']) {
if (url.searchParams.get(key) && !(url.pathname === '/admin/cs_csc_list.php' && key === 'f_act' && url.searchParams.get(key) === 'search')) ok = false;
}
if (url.pathname === '/include/ajaxReqJob.php') {
ok = [...url.searchParams.keys()].every(key => ['cs_id', 'csc_id', 'cs_id_photo', 'csc_id_ask_mail', 'cs_id_ask_mail', 'hq_id', 'vht_id', 'csc_id_payer_cash', '_'].includes(key)) && ['cs_id', 'csc_id', 'cs_id_photo', 'csc_id_ask_mail'].some(key => url.searchParams.has(key));
}
if (url.pathname === '/include/ajaxReqDocument.php') ok = url.searchParams.get('mode') === '1';
if (url.pathname === '/jobs/get_tournames.php') ok = [...url.searchParams.keys()].every(key => key === 'csc_id') && /^\d+$/.test(url.searchParams.get('csc_id') ?? '');
if (url.pathname === '/admin/jb_detail.php') ok = [...url.searchParams.keys()].every(key => key === 'job_id') && readableOrders.has(url.searchParams.get('job_id') ?? '');
if (url.pathname === '/include/ajaxReqGeneric.php') ok = [...url.searchParams.keys()].every(key => ['incFile', 'fun', 'par_01', 'retIdx', 'retParName', '_'].includes(key)) && url.searchParams.get('incFile') === 'inc_job.inc' && url.searchParams.get('fun') === 'getJobInternalRemarkItems' && readableOrders.has(url.searchParams.get('par_01') ?? '') && url.searchParams.get('retIdx') === '0' && url.searchParams.get('retParName') === '_RAW_TEXT_';
if (url.pathname === '/include/ajaxReqLib.php') {
ok = ['2', '6'].includes(url.searchParams.get('mode') ?? '');
if (!ok) denied.push('Library mode=' + url.searchParams.get('mode') + ' field=' + url.searchParams.get('value_03'));
}
} else if (method === 'POST' && url.pathname === '/jobs/job_options.php') {
const data = formData(request.postData() ?? '');
ok = data.get('getPriceOnly') === '1' && data.get('sendmail') !== '1';
if (!ok && creationMarker && saveAttempts === 0) {
const problems = validateCreation(data, creationMarker);
if (problems.length) denied.push('Order creation rejected: ' + problems.join(', '));
else {
saveAttempts++;
recordOrder({ state: 'submit-attempt', marker: creationMarker });
ok = true;
}
}
} else if (method === 'POST' && url.pathname === '/admin/cs_csc_list.php') {
const data = new URLSearchParams(request.postData() ?? '');
ok = data.get('f_act') === 'search';
} else if (login && method === 'POST' && url.pathname === '/admin/login.php') {
const data = new URLSearchParams(request.postData() ?? '');
ok = data.get('f_act') === '' && data.has('f_chk_account') && data.has('f_chk_password');
}
} else if (method === 'GET' && ['maps.googleapis.com', 'maps.gstatic.com', 'fonts.googleapis.com', 'fonts.gstatic.com'].includes(url.hostname)) {
ok = ['script', 'stylesheet', 'font', 'image'].includes(request.resourceType());
}
if (ok) {
allowed.push(description);
await route.continue();
} else {
denied.push(description);
await route.abort('blockedbyclient');
}
});
return {
denied, allowed, errors, knownErrors, suppressed, consoleErrors,
armCreation(marker: string) {
if (process.env.VOTIAN_UI_CREATE_ORDER !== '1' || !/^UI-TEST-\d{8}T\d{6}-[a-f0-9]{6}$/.test(marker) || creationMarker) throw new Error('Creation is not enabled or already armed');
const ledger = ledgerDirectory + 'created-orders.jsonl';
if (existsSync(ledger)) {
const events = readFileSync(ledger, 'utf8').trim().split('\n').filter(Boolean).map(line => JSON.parse(line));
const unresolved = events.find(event => event.state === 'submit-attempt' && !events.some(later => later.marker === event.marker && later.state === 'server-confirmed'));
if (unresolved) throw new Error('Unconfirmed previous save for ' + unresolved.marker + '. Inspect the server before another creation; see reports/created-orders.jsonl.');
}
creationMarker = marker;
},
allowOrderRead(id: string) { if (!/^\d+$/.test(id)) throw new Error('Invalid order ID'); readableOrders.add(id); },
get saveAttempts() { return saveAttempts; },
};
}