Steuerkram an den e2e-runner angebunden (tests/e2e/, 44 Playwright-Tests). Der erste Smoke-Lauf fand zwei Seiten mit HTTP 500: AccountController::create las $_GET['from'] ungeprüft, assets/form.php nutzte $testMode und $defaultKontoId außerhalb des Bearbeiten-Zweigs. Beides war bis zur Fehler-Härtung (#142) nur eine stille Warning. Test 67 räumt sein Belegverzeichnis jetzt mit ab - es blieb als root-eigenes Verzeichnis liegen und blockierte spätere echte Uploads mit HTTP 500. Test 77 (neu) sichert die Löschsperre von Beleg::delete() ab.
106 lines
4.7 KiB
JavaScript
106 lines
4.7 KiB
JavaScript
// @ts-check
|
|
// Hotspot: Bankimport-Vorschau.
|
|
//
|
|
// Die Vorschau wird vollständig im Browser aufgebaut: Die Datei geht per fetch()
|
|
// an /bank/detect-format, das Ergebnis wird per innerHTML in die Seite geschrieben
|
|
// (src/Views/bank/upload.php). Genau hier saß das DOM-XSS aus Issue #133 — ein
|
|
// serverseitiger Test kann diesen Pfad grundsätzlich nicht erreichen.
|
|
//
|
|
// Diese Tests brechen bewusst VOR dem Bestätigen ab: Die Vorschau allein legt
|
|
// keine Banktransaktionen an, der Datenbestand bleibt unangetastet.
|
|
|
|
const { test, expect } = require('@playwright/test')
|
|
const { uniqueTag, gotoOk } = require('../helpers/app')
|
|
|
|
/** CSV im Comdirect-Format: 4 zu überspringende Zeilen, dann Header, dann Daten. */
|
|
function comdirectCsv(buchungstext, betrag = '-119,00') {
|
|
return [
|
|
'Umsätze Girokonto;',
|
|
'"Zeitraum:";"30 Tage";',
|
|
'"Neuer Kontostand";"1.234,56 EUR";',
|
|
'',
|
|
'"Buchungstag";"Wertstellung (Valuta)";"Vorgang";"Buchungstext";"Umsatz in EUR";',
|
|
`"15.05.2026";"15.05.2026";"Lastschrift";"${buchungstext}";"${betrag}";`,
|
|
'"14.05.2026";"14.05.2026";"Gutschrift";"Zahlungseingang Kunde";"500,00";',
|
|
'',
|
|
].join('\n')
|
|
}
|
|
|
|
/** Datei in die Upload-Fläche legen und auf die fertige Formaterkennung warten. */
|
|
async function datenVorschauLaden(page, csvInhalt, dateiname = 'e2e-bank.csv') {
|
|
await gotoOk(page, '/bank/upload')
|
|
await page.setInputFiles('#bank-dnd-input', {
|
|
name: dateiname,
|
|
mimeType: 'text/csv',
|
|
buffer: Buffer.from(csvInhalt, 'utf-8'),
|
|
})
|
|
// Die Vorschau erscheint erst, wenn der fetch auf /bank/detect-format durch ist.
|
|
await expect(page.locator('#preview-firstrow')).toBeVisible({ timeout: 15000 })
|
|
}
|
|
|
|
test.describe('Bankimport-Vorschau', () => {
|
|
test('Vorschau zeigt die erste Datenzeile und aktiviert den Import-Button', async ({ page }) => {
|
|
const tag = uniqueTag()
|
|
await datenVorschauLaden(page, comdirectCsv(`Lieferant ${tag}`))
|
|
|
|
await expect(page.locator('#preview-firstrow')).toContainText(tag)
|
|
await expect(page.locator('#preview-firstrow')).toContainText('15.05.2026')
|
|
await expect(page.locator('#map-wert')).toContainText('-119,00')
|
|
|
|
// Erkanntes Format muss gesetzt und der Import damit möglich sein.
|
|
await expect(page.locator('#bank-upload-btn')).toBeEnabled()
|
|
expect(await page.locator('#format_id').inputValue()).not.toBe('')
|
|
})
|
|
|
|
test('Skriptcode im Buchungstext wird nicht ausgeführt (Regression #133)', async ({ page }) => {
|
|
const marker = uniqueTag('XSS')
|
|
// Zwei Vektoren: ein Element mit Error-Handler und ein direktes Script-Tag.
|
|
const payload = `<img src=x onerror="window.__e2eXss=true"><script>window.__e2eXss=true<\/script>${marker}`
|
|
|
|
/** @type {string[]} */
|
|
const dialoge = []
|
|
page.on('dialog', async d => { dialoge.push(d.message()); await d.dismiss() })
|
|
|
|
await datenVorschauLaden(page, comdirectCsv(payload))
|
|
|
|
// 1. Kein Skript darf gelaufen sein.
|
|
expect(await page.evaluate(() => window.__e2eXss === true), 'Injizierter Code wurde ausgeführt').toBe(false)
|
|
expect(dialoge, 'Unerwarteter Dialog aus injiziertem Code').toEqual([])
|
|
|
|
// 2. Der Payload darf nicht als DOM-Element gelandet sein.
|
|
expect(await page.locator('#preview-firstrow img').count(), 'Payload wurde als Element geparst').toBe(0)
|
|
expect(await page.locator('#preview-firstrow script').count()).toBe(0)
|
|
|
|
// 3. Er muss als sichtbarer Text erscheinen — escaped, nicht verschluckt.
|
|
await expect(page.locator('#preview-firstrow')).toContainText(marker)
|
|
await expect(page.locator('#preview-firstrow')).toContainText('<img')
|
|
})
|
|
|
|
test('Skriptcode im Mapping-Feld Buchungstext bleibt Text', async ({ page }) => {
|
|
const marker = uniqueTag('MAP')
|
|
const payload = `<b>${marker}</b>`
|
|
|
|
await datenVorschauLaden(page, comdirectCsv(payload))
|
|
|
|
// #map-buchungstext wird per textContent gesetzt — kein <b> im DOM.
|
|
await expect(page.locator('#map-buchungstext')).toContainText(marker)
|
|
expect(await page.locator('#map-buchungstext b').count(), 'Markup wurde interpretiert').toBe(0)
|
|
await expect(page.locator('#map-buchungstext')).toContainText('<b>')
|
|
})
|
|
|
|
test('Unstrukturierte Datei bringt die Vorschau nicht zum Absturz', async ({ page }) => {
|
|
await gotoOk(page, '/bank/upload')
|
|
await page.setInputFiles('#bank-dnd-input', {
|
|
name: 'kaputt.csv',
|
|
mimeType: 'text/csv',
|
|
buffer: Buffer.from('nur eine zeile ohne struktur', 'utf-8'),
|
|
})
|
|
|
|
// Erkennt die App kein passendes Format, fällt sie bewusst auf das erste
|
|
// bekannte zurück (siehe upload.php) — der Import bleibt also möglich.
|
|
// Geprüft wird deshalb nur, dass nichts abstürzt und die Seite bedienbar bleibt.
|
|
await page.waitForTimeout(3000)
|
|
await expect(page.locator('body')).not.toContainText('Ein Fehler ist aufgetreten')
|
|
await expect(page.locator('#fmt-select')).toBeVisible()
|
|
})
|
|
})
|