Finanzmanager/tests/e2e/specs/module.spec.js
fds2610-bot 0b4a216954 Sammel-Commit: PayPal-Bankverbindung und Bankleitzahlen-Verzeichnis
Zwei Features an derselben Stelle, die im Arbeitsverzeichnis
nebeneinander lagen und sich dieselben Dateien teilen (Routen,
BankingController, Bankzugang, Zugangsformular, Dokumentation) –
getrennte Commits ließen sich daraus nur mit einem kaputten
Zwischenstand schneiden.

PayPal als zweite Art von Bankverbindung
----------------------------------------
„Bankverbindung anlegen“ führt zur Wahl der Art: HBCI/FinTS oder
PayPal (bank_zugaenge.typ, Migration 014). Die Abfrage läuft über die
Classic-NVP-API (Services\PaypalNvp, TransactionSearch und GetBalance).
Kein Dreischritt, da PayPal genau ein Guthabenkonto führt –
Zugangsdaten und Kontenzuordnung stehen in einem Formular. Konten,
Umsätze, Protokoll, Regeln und Abgleich bleiben dieselben.

Bankleitzahlen-Verzeichnis
--------------------------
Verwaltung → Bankleitzahlen (/verwaltung/bankinstitute, Migration 015)
nimmt die CSV-Institutsliste der Deutschen Kreditwirtschaft auf und
beantwortet damit die eine Angabe, die beim Anlegen einer
FinTS-Verbindung regelmäßig fehlt: den PIN/TAN-Zugangspunkt.

Reine Nachschlagedaten ohne benutzer_id; ein Import ersetzt den
gesamten Bestand, statt ihn zu ergänzen. „Nur prüfen“ und „Übernehmen“
sind zwei Knöpfe an derselben Datei – abgelegt wird sie nicht, ihr
Inhalt steht ja in der Tabelle.

Services\FintsInstituteImport nimmt der Datei ihre Eigenheiten ab:
ISO-8859-1 statt UTF-8, ein Institutsname über zwei Zeilen in
Anführungszeichen, dieselbe Bankleitzahl vielfach (eine Zeile je
Filiale), Hunderte leerer Zeilen am Ende, Text statt Adresse, wo es
keinen PIN/TAN-Zugang gibt. Spalten werden über ihre Überschrift
angesprochen; fehlen BLZ und Institut, bricht der Import ab.

Gesucht wird über Bankleitzahl, BIC, Name und Ort in einem Zug: „1822“
steht bei der 1822direkt im Namen und bei der Frankfurter Sparkasse im
BIC, in keiner der beiden Bankleitzahlen.

Aus der Trefferliste führt „Verbindung anlegen“ ins gefüllte
FinTS-Formular. Die FinTS-Adresse ist damit kein Pflichtfeld mehr –
bleibt sie leer, wird sie zur Bankleitzahl nachgeschlagen. Findet auch
das nichts, weist Bankzugang den Zugang ab, statt ihn ohne Gegenstelle
zu speichern.

Tests: 31_PaypalZugangTest, 32_BankinstituteTest,
e2e/specs/banking.spec.js, e2e/specs/bankinstitute.spec.js.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 08:48:14 +02:00

293 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @ts-check
const { test, expect } = require('@playwright/test')
const BENUTZER = process.env.E2E_USER || 'e2e'
const PASSWORT = process.env.E2E_PASSWORD || 'e2e-testpasswort'
async function anmelden(page) {
await page.goto('/login')
if (page.url().includes('/einrichtung')) {
throw new Error(
'Instanz ist noch nicht eingerichtet. Zuerst einen Benutzer anlegen: ' +
'docker compose exec -u www-data php php scripts/e2e-benutzer.php'
)
}
await page.fill('#benutzername', BENUTZER)
await page.fill('#passwort', PASSWORT)
await page.click('button[type="submit"]')
await expect(page).toHaveURL(/\/$/)
}
/**
* Die Module aus den Phasen 4 bis 7: Cockpit-Kacheln, Online-Banking,
* Wertpapiere und die Nebenmodule.
*
* Geprüft wird, dass jeder Bereich erreichbar ist und seine Kernansicht
* aufbaut – nicht die Rechenlogik. Die steckt in der PHP-Suite, wo sie
* schneller und isoliert läuft.
*/
test.describe('Module der Phasen 4 bis 7', () => {
test.beforeEach(async ({ page }) => {
await anmelden(page)
})
test('Jeder Navigationspunkt führt in ein gebautes Modul', async ({ page }) => {
const punkte = [
['/konten', 'Kontenübersicht'],
['/banking', 'Bankverbindungen'],
['/zahlungsverkehr', 'Zahlungsverkehr'],
['/auswertungen', 'Auswertungen'],
['/planung', 'Planung'],
['/termine', 'Regelmäßige Buchungen'],
['/vertraege', 'Verträge'],
['/wertpapiere', 'Wertpapiere'],
['/verwaltung', 'Verwaltung'],
]
// Jeder Bereich hat sein Modul: Die Überschrift gehört zum Modul, nicht
// zur früheren Platzhalterseite.
for (const [pfad, titel] of punkte) {
await page.goto(pfad)
await expect(page.locator('h1')).toContainText(titel)
}
})
test('Das Cockpit zeichnet seine Kacheln', async ({ page }) => {
await page.goto('/')
// Über die Überschriften, nicht über den Kacheltext: „Kontostände“ kommt
// auch im Hinweis der Vermögenskachel vor.
const ueberschriften = page.locator('.kachel h2')
for (const titel of [
'Vermögensentwicklung', 'Einnahmen und Ausgaben', 'Meine Ausgaben', 'Kontostände',
]) {
await expect(ueberschriften.filter({ hasText: titel })).toHaveCount(1)
}
})
test('Abgewählte Kacheln verschwinden und kommen zurück', async ({ page }) => {
await page.goto('/einstellungen?bereich=darstellung')
const kachel = page.locator('input[name="cockpit_kacheln[]"][value="ausgaben"]')
await kachel.uncheck()
await page.click('button:has-text("Speichern")')
await page.goto('/')
await expect(page.locator('.kachel h2', { hasText: 'Meine Ausgaben' })).toHaveCount(0)
await expect(page.locator('.kachelfuss')).toContainText('abgeschaltet')
await page.goto('/einstellungen?bereich=darstellung')
await page.locator('input[name="cockpit_kacheln[]"][value="ausgaben"]').check()
await page.click('button:has-text("Speichern")')
await page.goto('/')
await expect(page.locator('.kachel h2', { hasText: 'Meine Ausgaben' })).toHaveCount(1)
})
test('Eine Bankverbindung lässt sich anlegen und wieder löschen', async ({ page }) => {
// Vor den drei Einrichtungsschritten steht die Wahl der Art; die
// Bankenmaske hängt an der ersten der beiden Karten.
await page.goto('/banking/neu')
await page.click('.formatkarte:has-text("HBCI/FinTS")')
await page.fill('#name', 'E2E-Testbank')
await page.fill('#blz', '12030000')
await page.fill('#fints_url', 'https://banking.example.org/fints')
await page.fill('#benutzerkennung', 'e2e-kunde')
await page.fill('#pin', 'geheim')
// Schritt 1 speichert nur die Zugangsdaten.
await expect(page.locator('.schrittmarke')).toContainText('Schritt 1 von 3')
await page.click('button:has-text("Weiter")')
// Schritt 2 fragt die Bank selbst nach ihren TAN-Verfahren. Die Adresse
// im Test führt nirgendwohin – geprüft wird deshalb der Fehlerweg: Die
// Seite erklärt, was zu tun ist, statt stehen zu bleiben.
await expect(page.locator('.schrittmarke')).toContainText('Schritt 2 von 3')
await expect(page.locator('h1')).toContainText('E2E-Testbank')
await expect(page.locator('.meldung-fehler')).toBeVisible()
await expect(page.locator('a:has-text("Zugangsdaten prüfen")')).toHaveCount(1)
// Wer die Einrichtung vorzeitig verlässt, landet in der Übersicht: Die
// Zugangsmaske böte nur noch an, was gerade durchlaufen wurde.
await page.click('a:has-text("Später einstellen")')
await expect(page.locator('h1')).toContainText('Bankverbindungen')
await expect(page.locator('td a:has-text("E2E-Testbank")')).toHaveCount(1)
await page.click('td a:has-text("E2E-Testbank")')
await expect(page.locator('h1')).toContainText('E2E-Testbank')
// Die PIN wird nie wieder angezeigt.
await expect(page.locator('#pin')).toHaveValue('')
await expect(page.locator('#pin')).toHaveAttribute('placeholder', /unverändert/)
// Ohne Wahl bleibt der Hinweis stehen, und die Seite ist wieder erreichbar.
await expect(page.locator('a:has-text("TAN-Verfahren einstellen")')).toHaveCount(1)
// Der Stichtag gibt es zweimal: als Vorgabe des Zugangs …
await page.selectOption('#abrufzeitraum', 'stichtag')
await page.fill('#abruf_stichtag', '2026-01-01')
await page.click('button:has-text("Speichern")')
await expect(page.locator('.meldung')).toContainText('gespeichert')
await expect(page.locator('#abruf_stichtag')).toHaveValue('2026-01-01')
// … und als einmalige Angabe neben dem Abrufknopf, die nichts speichert.
await expect(page.locator('.abrufform input[name="stichtag"]')).toHaveCount(1)
await page.goto('/banking')
await expect(page.locator('tr:has-text("E2E-Testbank")')).toContainText('Ab Stichtag')
await expect(page.locator('tr:has-text("E2E-Testbank")')).toContainText('01.01.2026')
await page.click('td a:has-text("E2E-Testbank")')
// Schritt 3: die Kontenzuordnung. Ohne erreichbare Bank kennt sie noch
// kein Konto und bietet die Abfrage an, statt eine leere Tabelle zu zeigen.
await page.click('a:has-text("Konten zuordnen")')
await expect(page.locator('h1')).toContainText('Konten zuordnen')
await expect(page.locator('.platzhalter')).toContainText('Noch kein Konto bekannt')
await expect(page.locator('button:has-text("Konten bei der Bank abfragen")')).toHaveCount(1)
await page.click('a:has-text("Zurück zur Bankverbindung")')
await expect(page.locator('h1')).toContainText('E2E-Testbank')
page.once('dialog', dialog => dialog.accept())
await page.click('button:has-text("Bankverbindung löschen")')
await expect(page.locator('.meldung')).toContainText('gelöscht')
})
test('Eine falsche IBAN wird beim Anlegen einer Adresse abgewiesen', async ({ page }) => {
await page.goto('/vertraege/adressen')
await page.fill('#name', 'E2E-Falschtest')
await page.fill('#iban', 'DE02120300000000202052')
await page.click('button:has-text("Adresse anlegen")')
await expect(page.locator('.meldung-fehler')).toContainText('IBAN')
})
test('Ein Kredit zeigt seinen gerechneten Tilgungsplan', async ({ page }) => {
await page.goto('/vertraege/kredite')
await page.fill('#name', 'E2E-Darlehen')
await page.fill('#darlehensbetrag', '100000')
await page.fill('#zinssatz', '3,0')
await page.fill('#rate', '500')
await page.click('button:has-text("Kredit anlegen")')
await expect(page.locator('h3', { hasText: 'Tilgungsplan' })).toContainText('E2E-Darlehen')
await expect(page.locator('.kennzahltitel', { hasText: 'Zinsen gesamt' })).toHaveCount(1)
// Erste Rate: 250 Zinsen, 250 Tilgung. Die erste Tabelle ist die
// Kreditliste, der Plan steht in der zweiten.
const ersteRate = page.locator('table.liste').last().locator('tbody tr').first()
await expect(ersteRate).toContainText('250,00')
page.once('dialog', dialog => dialog.accept())
await page.locator('tr', { hasText: 'E2E-Darlehen' }).locator('button:has-text("Löschen")').first().click()
await expect(page.locator('.meldung')).toContainText('gelöscht')
})
test('Depotbuchungen lassen sich über die Auswahl sammelweise löschen', async ({ page }) => {
const marke = Date.now().toString().slice(-6)
const depot = `E2E Depot ${marke}`
const papier = `E2E Papier ${marke}`
await page.goto('/konten/neu')
await page.fill('#name', depot)
await page.selectOption('#kontoart', 'depot')
await page.click('.formular button[type="submit"]')
await expect(page.locator('.meldung-erfolg')).toBeVisible()
// Zwei Käufe desselben Papiers – der zweite über die Auswahlliste, damit
// kein zweites Wertpapier entsteht.
for (const [nr, stueck] of [[1, '10'], [2, '5']]) {
await page.goto('/wertpapiere/buchungen/neu')
await page.selectOption('#konto_id', { label: depot })
await page.fill('#datum', '2026-05-0' + nr)
if (nr === 1) {
await page.fill('#wertpapier_name', papier)
} else {
await page.selectOption('#wertpapier_id', { label: papier })
}
await page.fill('#stueck', stueck)
await page.fill('#kurs', '100')
await page.click('.formular button[type="submit"]')
await expect(page.locator('.meldung-erfolg')).toBeVisible()
}
// Der Filter bleibt über das Löschen hinweg stehen – das Formularziel
// trägt ihn mit.
await page.goto('/wertpapiere/buchungen')
await page.selectOption('select[name="depot"]', { label: depot })
await page.click('button:has-text("Filtern")')
await expect(page.locator('tbody tr')).toHaveCount(2)
// Der Bestand kennt beide Käufe.
await page.goto('/wertpapiere')
await page.selectOption('select[name="depot"]', { label: depot })
await page.click('button:has-text("Anzeigen")')
await expect(page.locator('tbody tr', { hasText: papier })).toContainText('15')
await page.goBack()
await page.goBack()
// Ohne Auswahl bleibt der Knopf gesperrt.
const knopf = page.locator('#depotbuchungsauswahl button')
await expect(knopf).toBeDisabled()
await page.check('.alle-waehlen')
await expect(knopf).toContainText('(2)')
const gefragt = []
page.on('dialog', async (dialog) => {
gefragt.push(dialog.message())
await dialog.accept()
})
await knopf.click()
expect(gefragt.join(' ')).toContain('löschen')
await expect(page.locator('.meldung-erfolg')).toContainText('gelöscht')
await expect(page).toHaveURL(/depot=/)
// Der Name steht weiter im Filterfeld – gemeint ist die Liste.
await expect(page.locator('table.liste')).toHaveCount(0)
await expect(page.locator('.platzhalter')).toContainText('Keine Depotbuchungen')
// Und der Bestand ist die Buchungen los – er wird gerechnet.
await page.goto('/wertpapiere')
await page.selectOption('select[name="depot"]', { label: depot })
await page.click('button:has-text("Anzeigen")')
await expect(page.locator('.platzhalter')).toContainText('Keine Bestände')
// Aufräumen: Das Testdepot wieder entfernen. Die Rückfrage übernimmt der
// Dialoghandler von oben.
await page.goto('/konten')
const pfad = await page.locator('a', { hasText: depot }).first().getAttribute('href')
await page.goto(pfad + '/bearbeiten')
await page.locator('button:has-text("Konto löschen")').click()
await expect(page.locator('.meldung')).toContainText('gelöscht')
})
test('Der Finanzkalender ist erreichbar und lässt sich einstellen', async ({ page }) => {
await page.goto('/termine/kalender')
await expect(page.locator('h1')).toContainText('Finanzkalender')
await page.selectOption('#tage', '90')
await page.click('button:has-text("Anzeigen")')
await expect(page).toHaveURL(/tage=90/)
})
test('Der Zahlungsverkehr weist eine falsche IBAN ab', async ({ page }) => {
await page.goto('/zahlungsverkehr/neu')
await page.fill('#empfaenger', 'E2E-Empfänger')
await page.fill('#iban', 'DE00000000000000000000')
await page.fill('#betrag', '10,00')
await page.click('button:has-text("Erfassen")')
// Die Bankverbindung ist bewusst nicht Pflicht: Ein Auftrag darf erfasst
// werden, bevor eine eingerichtet ist. Geprüft wird sie erst beim Senden.
await expect(page.locator('.meldung-fehler')).toContainText('IBAN')
})
})