Steuerkram/tests/e2e/specs/bank-import.spec.js
fds2610-bot 7cea573ffc Fix #e2e: E2E-Tests ergänzt, /accounts/new und /assets/new lieferten HTTP 500
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.
2026-08-02 11:58:33 +02:00

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()
})
})