Fix #3: Integrationstests und Test-Runner für alle Kernfunktionen erstellt
This commit is contained in:
parent
08b99e3992
commit
632cbeea12
10 changed files with 917 additions and 0 deletions
44
CLAUDE.md
Normal file
44
CLAUDE.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# Steuerkram – Entwicklungsregeln
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
PHP 8.3+, MySQL, PSR-4 (Namespace `Steuerkram\`), Composer, kein Framework.
|
||||||
|
MVC: `src/Controllers/`, `src/Models/`, `src/Services/`, `src/Views/`.
|
||||||
|
|
||||||
|
## Coding-Regeln
|
||||||
|
- Kein `var_dump`, kein `die()` in Produktionscode.
|
||||||
|
- Neue Libraries nur nach expliziter Rückfrage einbinden.
|
||||||
|
- Bestehenden Code-Stil beibehalten (declare strict_types, 4 Spaces).
|
||||||
|
- SQL ausschließlich über `Database::fetchAll/fetchOne/execute()` mit Prepared Statements.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
**Regel: Jedes neue Feature bekommt einen eigenen Test.**
|
||||||
|
|
||||||
|
Tests liegen in `tests/` und folgen dem Namensschema `NN_FunktionsnameTest.php`.
|
||||||
|
Jede Testklasse erweitert `TestCase` und implementiert `run()`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
php tests/run.php # alle Tests ausführen
|
||||||
|
./scripts/run-tests.sh # Tests + automatische Issue-Erstellung bei Fehlern
|
||||||
|
./scripts/run-tests.sh 42 # Tests; bei Fehlern Kommentar in Issue #42
|
||||||
|
```
|
||||||
|
|
||||||
|
### Neuen Test erstellen
|
||||||
|
1. Datei `tests/NN_MeinFeatureTest.php` anlegen (NN = nächste freie Nummer)
|
||||||
|
2. Klasse `MeinFeatureTest extends TestCase`
|
||||||
|
3. `protected string $testName = 'Test NN: Beschreibung';`
|
||||||
|
4. Methode `public function run(): void` mit Assertions implementieren
|
||||||
|
5. Transaktions-Isolation ist automatisch (setUp/tearDown in TestCase)
|
||||||
|
|
||||||
|
### Assertion-Methoden
|
||||||
|
```php
|
||||||
|
$this->assert(bool, 'Beschreibung');
|
||||||
|
$this->assertEquals($erwartet, $ist, 'Beschreibung');
|
||||||
|
$this->assertGreaterThan($min, $wert, 'Beschreibung');
|
||||||
|
$this->assertNotEmpty($wert, 'Beschreibung');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commits
|
||||||
|
```
|
||||||
|
Fix #NR: Kurze Beschreibung der Änderung
|
||||||
|
```
|
||||||
|
Keine Co-Authored-By-Zeile. Keine langen Commit-Messages.
|
||||||
|
|
@ -9,6 +9,8 @@ services:
|
||||||
- ./migrations:/var/www/html/migrations
|
- ./migrations:/var/www/html/migrations
|
||||||
- uploads:/var/www/html/public/static/uploads
|
- uploads:/var/www/html/public/static/uploads
|
||||||
- ./data:/var/www/html/data
|
- ./data:/var/www/html/data
|
||||||
|
- ./tests:/var/www/html/tests
|
||||||
|
- ./scripts:/var/www/html/scripts
|
||||||
environment:
|
environment:
|
||||||
DB_HOST: db
|
DB_HOST: db
|
||||||
DB_NAME: steuerkram
|
DB_NAME: steuerkram
|
||||||
|
|
|
||||||
123
scripts/run-tests.sh
Executable file
123
scripts/run-tests.sh
Executable file
|
|
@ -0,0 +1,123 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# ============================================================
|
||||||
|
# Steuerkram Test-Runner mit automatischer Issue-Erstellung
|
||||||
|
#
|
||||||
|
# Verwendung:
|
||||||
|
# ./scripts/run-tests.sh [ISSUE_NUMMER]
|
||||||
|
#
|
||||||
|
# Parameter:
|
||||||
|
# ISSUE_NUMMER (optional) Nummer des aktuell bearbeiteten Issues,
|
||||||
|
# auf das bei neu erstellten Bug-Issues verwiesen wird.
|
||||||
|
#
|
||||||
|
# Umgebungsvariablen:
|
||||||
|
# CODEBERG_TOKEN Codeberg-API-Token (für Issue-Erstellung)
|
||||||
|
#
|
||||||
|
# Exit-Code: 0 = alle Tests OK, 1 = Fehler gefunden
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||||
|
CURRENT_ISSUE="${1:-}"
|
||||||
|
REPO="fds2610/Steuerkram"
|
||||||
|
API_BASE="https://codeberg.org/api/v1"
|
||||||
|
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
|
||||||
|
# ── Tests ausführen ──────────────────────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "Führe Tests aus..."
|
||||||
|
JSON_OUTPUT=$(php tests/run.php --json 2>&1) || true
|
||||||
|
EXIT_CODE=$?
|
||||||
|
|
||||||
|
# Auch die lesbare Ausgabe zeigen
|
||||||
|
php tests/run.php || true
|
||||||
|
|
||||||
|
# ── Ergebnis auswerten ───────────────────────────────────────
|
||||||
|
FAILED=$(echo "$JSON_OUTPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('failed',0))" 2>/dev/null || echo "0")
|
||||||
|
FAILURES_JSON=$(echo "$JSON_OUTPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(json.dumps(d.get('failures',[])))" 2>/dev/null || echo "[]")
|
||||||
|
|
||||||
|
if [ "$FAILED" = "0" ] || [ "$FAILED" = "" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "✓ Alle Tests bestanden. Commit kann fortgesetzt werden."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✗ ${FAILED} Test(s) fehlgeschlagen."
|
||||||
|
|
||||||
|
# ── Issues erstellen (nur wenn Token vorhanden) ──────────────
|
||||||
|
if [ -z "${CODEBERG_TOKEN:-}" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "Hinweis: CODEBERG_TOKEN nicht gesetzt – keine automatischen Issues erstellt."
|
||||||
|
echo "Fehlgeschlagene Tests:"
|
||||||
|
echo "$FAILURES_JSON" | python3 -c "import json,sys; [print(' •', f) for f in json.load(sys.stdin)]" 2>/dev/null || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Erstelle automatische Bug-Issues auf Codeberg..."
|
||||||
|
|
||||||
|
CREATED_ISSUES=()
|
||||||
|
|
||||||
|
# Jede Fehlermeldung als eigenes Issue anlegen
|
||||||
|
while IFS= read -r failure; do
|
||||||
|
[ -z "$failure" ] && continue
|
||||||
|
|
||||||
|
# Titel kürzen (max. 100 Zeichen)
|
||||||
|
TITLE="[BUG] Test fehlgeschlagen: ${failure:0:80}"
|
||||||
|
|
||||||
|
# Body zusammenbauen
|
||||||
|
BODY="## Automatisch erkannter Testfehler\n\n"
|
||||||
|
BODY+="**Fehlermeldung:** \`${failure}\`\n\n"
|
||||||
|
BODY+="**Erkannt am:** $(date '+%Y-%m-%d %H:%M:%S')\n\n"
|
||||||
|
BODY+="**Commit:** $(git rev-parse --short HEAD 2>/dev/null || echo 'unbekannt')\n\n"
|
||||||
|
|
||||||
|
if [ -n "$CURRENT_ISSUE" ]; then
|
||||||
|
BODY+="Dieser Fehler wurde beim Bearbeiten von Issue #${CURRENT_ISSUE} entdeckt.\n\n"
|
||||||
|
fi
|
||||||
|
|
||||||
|
BODY+="### Schritte zur Reproduktion\n"
|
||||||
|
BODY+="1. \`php tests/run.php\` ausführen\n"
|
||||||
|
BODY+="2. Fehler tritt im oben genannten Test auf\n\n"
|
||||||
|
BODY+="_Dieser Issue wurde automatisch durch den Test-Runner erstellt._\n"
|
||||||
|
BODY+="_Das Label 'automate' wurde bewusst NICHT gesetzt (Human-in-the-loop)._"
|
||||||
|
|
||||||
|
RESPONSE=$(curl -s -X POST \
|
||||||
|
-H "Authorization: token $CODEBERG_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$(python3 -c "import json; print(json.dumps({'title': '${TITLE}', 'body': '${BODY}', 'labels': []}))")" \
|
||||||
|
"${API_BASE}/repos/${REPO}/issues")
|
||||||
|
|
||||||
|
ISSUE_NR=$(echo "$RESPONSE" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('number','?'))" 2>/dev/null || echo "?")
|
||||||
|
CREATED_ISSUES+=("#${ISSUE_NR}")
|
||||||
|
echo " → Issue #${ISSUE_NR} erstellt: ${failure:0:60}..."
|
||||||
|
|
||||||
|
done < <(echo "$FAILURES_JSON" | python3 -c "import json,sys; [print(f) for f in json.load(sys.stdin)]" 2>/dev/null)
|
||||||
|
|
||||||
|
# ── Kommentar im aktuellen Issue hinterlassen ─────────────────
|
||||||
|
if [ -n "$CURRENT_ISSUE" ] && [ ${#CREATED_ISSUES[@]} -gt 0 ]; then
|
||||||
|
ISSUE_LINKS=$(printf '%s, ' "${CREATED_ISSUES[@]}")
|
||||||
|
ISSUE_LINKS="${ISSUE_LINKS%, }"
|
||||||
|
|
||||||
|
COMMENT="### Automatische Testläufe\n\n"
|
||||||
|
COMMENT+="Beim Bearbeiten dieses Issues wurden **${FAILED} Test(s)** fehlgeschlagen.\n\n"
|
||||||
|
COMMENT+="Folgende Bug-Issues wurden automatisch erstellt (ohne 'automate'-Label – bitte manuell prüfen):\n\n"
|
||||||
|
for issue in "${CREATED_ISSUES[@]}"; do
|
||||||
|
COMMENT+="- ${issue}\n"
|
||||||
|
done
|
||||||
|
COMMENT+="\n_Commit: $(git rev-parse --short HEAD 2>/dev/null || echo 'unbekannt')_"
|
||||||
|
|
||||||
|
curl -s -X POST \
|
||||||
|
-H "Authorization: token $CODEBERG_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "$(python3 -c "import json; print(json.dumps({'body': '${COMMENT}'}))")" \
|
||||||
|
"${API_BASE}/repos/${REPO}/issues/${CURRENT_ISSUE}/comments" > /dev/null
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Kommentar mit Links zu Bug-Issues in Issue #${CURRENT_ISSUE} hinterlassen."
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 1
|
||||||
57
tests/01_BelegUploadTest.php
Normal file
57
tests/01_BelegUploadTest.php
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test 1: Beleg hochladen und in der Belegübersicht prüfen.
|
||||||
|
*
|
||||||
|
* Prüft, ob ein neu erstellter Beleg via Beleg::create() in
|
||||||
|
* Beleg::all() und Beleg::find() auffindbar ist.
|
||||||
|
*/
|
||||||
|
class BelegUploadTest extends TestCase
|
||||||
|
{
|
||||||
|
protected string $testName = 'Test 1: Beleg hochladen → Belegübersicht';
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$belegNr = 'TEST-' . uniqid();
|
||||||
|
$datum = date('Y-m-d');
|
||||||
|
|
||||||
|
$id = \Steuerkram\Models\Beleg::create([
|
||||||
|
'beleg_nr' => $belegNr,
|
||||||
|
'filename' => 'test-rechnung.pdf',
|
||||||
|
'storage_path' => 'static/uploads/test/test-rechnung.pdf',
|
||||||
|
'mime_type' => 'application/pdf',
|
||||||
|
'file_hash' => hash('sha256', $belegNr),
|
||||||
|
'beleg_datum' => $datum,
|
||||||
|
'eingangs_datum' => $datum,
|
||||||
|
'rechnungs_nr' => 'RE-TEST-001',
|
||||||
|
'betrag_brutto' => 119.00,
|
||||||
|
'belegtyp' => 'Eingangsrechnung',
|
||||||
|
'notes' => 'Automatisch erstellt (Test)',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertGreaterThan(0, $id, 'Beleg wurde in der DB angelegt (ID > 0)');
|
||||||
|
|
||||||
|
// Beleg via find() abrufbar
|
||||||
|
$beleg = \Steuerkram\Models\Beleg::find($id);
|
||||||
|
$this->assertNotEmpty($beleg, 'Beleg::find() liefert einen Datensatz');
|
||||||
|
$this->assertEquals($belegNr, $beleg['beleg_nr'] ?? null, 'Belegnummer stimmt überein');
|
||||||
|
$this->assertEquals('Eingangsrechnung', $beleg['belegtyp'] ?? null, 'Belegtyp ist Eingangsrechnung');
|
||||||
|
|
||||||
|
// Beleg erscheint in Beleg::all() (ohne Jahr-Filter, um sicher zu finden)
|
||||||
|
$result = \Steuerkram\Models\Beleg::all([], 1, 999);
|
||||||
|
$found = false;
|
||||||
|
foreach ($result['rows'] as $row) {
|
||||||
|
if ((int)$row['id'] === $id) {
|
||||||
|
$found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assert($found, 'Beleg erscheint in Beleg::all() (Belegübersicht)');
|
||||||
|
$this->assertGreaterThan(0, $result['total'], 'Belegübersicht enthält mindestens 1 Eintrag');
|
||||||
|
|
||||||
|
// Betrag korrekt gespeichert
|
||||||
|
$this->assertEquals(119.00, (float)($beleg['betrag_brutto'] ?? 0), 'Betrag brutto korrekt gespeichert');
|
||||||
|
}
|
||||||
|
}
|
||||||
110
tests/02_BelegBuchungTest.php
Normal file
110
tests/02_BelegBuchungTest.php
Normal file
|
|
@ -0,0 +1,110 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test 2: Beleg buchen und Buchung im Buchungsjournal prüfen.
|
||||||
|
*
|
||||||
|
* Erstellt einen Beleg und eine zugehörige Buchung, dann wird geprüft,
|
||||||
|
* ob die Buchung in Booking::journal() und Booking::forBeleg() auffindbar ist.
|
||||||
|
*/
|
||||||
|
class BelegBuchungTest extends TestCase
|
||||||
|
{
|
||||||
|
protected string $testName = 'Test 2: Beleg buchen → Buchungsjournal';
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Konten aus der SKR03-Seed-Datei abrufen
|
||||||
|
$aufwandId = $this->findAccountId('4920'); // Bürobedarf (Aufwand)
|
||||||
|
$bankId = $this->findAccountId('1200'); // Bank (Aktiv)
|
||||||
|
|
||||||
|
if (!$aufwandId || !$bankId) {
|
||||||
|
// Fallback: irgendein Aufwand- und Aktiv-Konto suchen
|
||||||
|
$aufwandRow = \Steuerkram\Database::fetchOne(
|
||||||
|
"SELECT id FROM accounts WHERE kontenart = 'Aufwand' AND aktiv = 1 LIMIT 1"
|
||||||
|
);
|
||||||
|
$bankRow = \Steuerkram\Database::fetchOne(
|
||||||
|
"SELECT id FROM accounts WHERE kontenart = 'Aktiv' AND aktiv = 1 LIMIT 1"
|
||||||
|
);
|
||||||
|
$aufwandId = $aufwandRow ? (int)$aufwandRow['id'] : null;
|
||||||
|
$bankId = $bankRow ? (int)$bankRow['id'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertNotEmpty($aufwandId, 'Aufwandskonto gefunden');
|
||||||
|
$this->assertNotEmpty($bankId, 'Aktiv-/Bankkonto gefunden');
|
||||||
|
|
||||||
|
if (!$aufwandId || !$bankId) {
|
||||||
|
return; // Voraussetzungen nicht erfüllt, weitere Asserts wären sinnlos
|
||||||
|
}
|
||||||
|
|
||||||
|
// Beleg anlegen
|
||||||
|
$belegNr = 'TEST-' . uniqid();
|
||||||
|
$datum = date('Y-m-d');
|
||||||
|
$belegId = \Steuerkram\Models\Beleg::create([
|
||||||
|
'beleg_nr' => $belegNr,
|
||||||
|
'filename' => 'eingangsrechnung-test.pdf',
|
||||||
|
'storage_path' => 'static/uploads/test/eingangsrechnung-test.pdf',
|
||||||
|
'mime_type' => 'application/pdf',
|
||||||
|
'file_hash' => hash('sha256', $belegNr . 'buch'),
|
||||||
|
'beleg_datum' => $datum,
|
||||||
|
'eingangs_datum' => $datum,
|
||||||
|
'rechnungs_nr' => 'RE-BUCH-001',
|
||||||
|
'betrag_brutto' => 59.50,
|
||||||
|
'belegtyp' => 'Eingangsrechnung',
|
||||||
|
]);
|
||||||
|
$this->assertGreaterThan(0, $belegId, 'Beleg für Buchungstest angelegt');
|
||||||
|
|
||||||
|
// Buchung erstellen
|
||||||
|
$buchungId = \Steuerkram\Models\Booking::create([
|
||||||
|
'beleg_id' => $belegId,
|
||||||
|
'buchungs_datum' => $datum,
|
||||||
|
'soll_konto_id' => $aufwandId,
|
||||||
|
'haben_konto_id' => $bankId,
|
||||||
|
'betrag_netto' => 50.00,
|
||||||
|
'ust_satz' => 19.0,
|
||||||
|
'buchungstext' => 'Testbuchung Bürobedarf',
|
||||||
|
]);
|
||||||
|
$this->assertGreaterThan(0, $buchungId, 'Buchung wurde angelegt (ID > 0)');
|
||||||
|
|
||||||
|
// Buchung via Booking::find() abrufbar
|
||||||
|
$buchung = \Steuerkram\Models\Booking::find($buchungId);
|
||||||
|
$this->assertNotEmpty($buchung, 'Booking::find() liefert die Buchung');
|
||||||
|
$this->assertEquals(50.00, (float)($buchung['betrag_netto'] ?? 0), 'Nettobetrag korrekt');
|
||||||
|
$this->assertEquals(19.0, (float)($buchung['ust_satz'] ?? 0), 'USt-Satz korrekt');
|
||||||
|
|
||||||
|
// Buchung via Booking::forBeleg() abrufbar
|
||||||
|
$buchungen = \Steuerkram\Models\Booking::forBeleg($belegId);
|
||||||
|
$this->assertGreaterThan(0, count($buchungen), 'Booking::forBeleg() liefert Buchungen');
|
||||||
|
|
||||||
|
$foundInBeleg = false;
|
||||||
|
foreach ($buchungen as $bk) {
|
||||||
|
if ((int)$bk['id'] === $buchungId) {
|
||||||
|
$foundInBeleg = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assert($foundInBeleg, 'Buchung erscheint in Booking::forBeleg()');
|
||||||
|
|
||||||
|
// Buchung im Journal
|
||||||
|
$year = (int)date('Y');
|
||||||
|
$month = (int)date('n');
|
||||||
|
$journal = \Steuerkram\Models\Booking::journal(['year' => $year, 'month' => $month], 1, 999);
|
||||||
|
$foundInJournal = false;
|
||||||
|
foreach ($journal['rows'] as $row) {
|
||||||
|
if ((int)$row['id'] === $buchungId) {
|
||||||
|
$foundInJournal = true;
|
||||||
|
// Vollständigkeit prüfen
|
||||||
|
$this->assertNotEmpty($row['buchungs_nr'], 'Buchungsnummer im Journal vorhanden');
|
||||||
|
$this->assertNotEmpty($row['soll_nr'], 'Soll-Kontonummer im Journal vorhanden');
|
||||||
|
$this->assertNotEmpty($row['haben_nr'], 'Haben-Kontonummer im Journal vorhanden');
|
||||||
|
$this->assertNotEmpty($row['beleg_nr'], 'Belegnummer im Journal vorhanden');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assert($foundInJournal, 'Buchung erscheint vollständig im Buchungsjournal');
|
||||||
|
|
||||||
|
// Beleg ist jetzt als gebucht markiert
|
||||||
|
$belegNeu = \Steuerkram\Models\Beleg::find($belegId);
|
||||||
|
$this->assertEquals(1, (int)($belegNeu['booked'] ?? 0), 'Beleg ist als gebucht markiert');
|
||||||
|
}
|
||||||
|
}
|
||||||
83
tests/03_AusgangsrechnungTest.php
Normal file
83
tests/03_AusgangsrechnungTest.php
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test 3: Ausgangsrechnung erstellen und in der Übersicht prüfen.
|
||||||
|
*
|
||||||
|
* Erstellt einen Testkunden und eine Ausgangsrechnung mit einer Position,
|
||||||
|
* dann wird geprüft ob die Rechnung in Invoice::all() und Invoice::find() erscheint.
|
||||||
|
*/
|
||||||
|
class AusgangsrechnungTest extends TestCase
|
||||||
|
{
|
||||||
|
protected string $testName = 'Test 3: Ausgangsrechnung erstellen → Übersicht';
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Testkunden anlegen
|
||||||
|
$customerId = \Steuerkram\Models\Customer::create([
|
||||||
|
'nachname' => 'Testmann',
|
||||||
|
'vorname' => 'Karl',
|
||||||
|
'firma' => 'Test GmbH',
|
||||||
|
'strasse' => 'Teststraße 1',
|
||||||
|
'plz' => '12345',
|
||||||
|
'ort' => 'Teststadt',
|
||||||
|
'land' => 'DE',
|
||||||
|
'payment_days' => 14,
|
||||||
|
]);
|
||||||
|
$this->assertGreaterThan(0, $customerId, 'Testkunde wurde angelegt');
|
||||||
|
|
||||||
|
// Ausgangsrechnung erstellen
|
||||||
|
$datum = date('Y-m-d');
|
||||||
|
$invoiceId = \Steuerkram\Models\Invoice::create([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'betreff' => 'Testrechnung Webentwicklung',
|
||||||
|
'rechnungs_datum' => $datum,
|
||||||
|
'leistungszeitraum_von' => date('Y-m-01'),
|
||||||
|
'leistungszeitraum_bis' => date('Y-m-t'),
|
||||||
|
'notes' => 'Automatisch erstellt (Test)',
|
||||||
|
'lines' => [
|
||||||
|
[
|
||||||
|
'bezeichnung' => 'Webentwicklung',
|
||||||
|
'menge' => 10,
|
||||||
|
'einheit' => 'Std.',
|
||||||
|
'einzelpreis' => 100.00,
|
||||||
|
'ust_satz' => 19.0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
$this->assertGreaterThan(0, $invoiceId, 'Rechnung wurde in der DB angelegt (ID > 0)');
|
||||||
|
|
||||||
|
// Rechnung via Invoice::find() abrufbar
|
||||||
|
$invoice = \Steuerkram\Models\Invoice::find($invoiceId);
|
||||||
|
$this->assertNotEmpty($invoice, 'Invoice::find() liefert einen Datensatz');
|
||||||
|
$this->assertEquals('Testrechnung Webentwicklung', $invoice['betreff'] ?? null, 'Betreff stimmt überein');
|
||||||
|
$this->assertEquals('Entwurf', $invoice['status'] ?? null, 'Status ist Entwurf');
|
||||||
|
$this->assertEquals($customerId, (int)($invoice['customer_id'] ?? 0), 'Kunde korrekt zugeordnet');
|
||||||
|
|
||||||
|
// Betragsberechnung prüfen
|
||||||
|
$erwNetto = 1000.00; // 10 Std. × 100 €
|
||||||
|
$erwUst = 190.00; // 19%
|
||||||
|
$erwBrutto = 1190.00;
|
||||||
|
$this->assertEquals($erwNetto, (float)($invoice['betrag_netto'] ?? 0), 'Nettobetrag korrekt berechnet');
|
||||||
|
$this->assertEquals($erwUst, (float)($invoice['betrag_ust'] ?? 0), 'USt-Betrag korrekt berechnet');
|
||||||
|
$this->assertEquals($erwBrutto, (float)($invoice['betrag_brutto'] ?? 0), 'Bruttobetrag korrekt berechnet');
|
||||||
|
|
||||||
|
// Positionen vorhanden
|
||||||
|
$lines = \Steuerkram\Models\Invoice::lines($invoiceId);
|
||||||
|
$this->assertEquals(1, count($lines), 'Rechnung hat genau 1 Position');
|
||||||
|
$this->assertEquals('Webentwicklung', $lines[0]['bezeichnung'] ?? null, 'Positionsbezeichnung korrekt');
|
||||||
|
|
||||||
|
// Rechnung in Invoice::all() auffindbar
|
||||||
|
$result = \Steuerkram\Models\Invoice::all([], 1, 999);
|
||||||
|
$found = false;
|
||||||
|
foreach ($result['rows'] as $row) {
|
||||||
|
if ((int)$row['id'] === $invoiceId) {
|
||||||
|
$found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assert($found, 'Rechnung erscheint in Invoice::all() (Rechnungsübersicht)');
|
||||||
|
$this->assertGreaterThan(0, $result['total'], 'Rechnungsübersicht enthält mindestens 1 Eintrag');
|
||||||
|
}
|
||||||
|
}
|
||||||
134
tests/04_InvoiceBuchungTest.php
Normal file
134
tests/04_InvoiceBuchungTest.php
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test 4: Ausgangsrechnung finalisieren und buchen → Buchungsjournal prüfen.
|
||||||
|
*
|
||||||
|
* Erstellt eine Rechnung, finalisiert sie (Entwurf → Gesendet) und erstellt
|
||||||
|
* die zugehörigen Buchungen. Dann wird geprüft, ob die Buchungszeilen
|
||||||
|
* im Buchungsjournal erscheinen.
|
||||||
|
*/
|
||||||
|
class InvoiceBuchungTest extends TestCase
|
||||||
|
{
|
||||||
|
protected string $testName = 'Test 4: Rechnung buchen → Buchungsjournal';
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
// Benötigte Konten prüfen (SKR03)
|
||||||
|
$debitorId = $this->findAccountId('1400'); // Debitorenkonto
|
||||||
|
$erloesId = $this->findAccountId('8000'); // Erlöse 19 %
|
||||||
|
|
||||||
|
if (!$debitorId) {
|
||||||
|
$this->assert(false, 'Debitorenkonto 1400 nicht gefunden – SKR03-Seed vorhanden?');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!$erloesId) {
|
||||||
|
$this->assert(false, 'Erlöskonto 8000 nicht gefunden – SKR03-Seed vorhanden?');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assert(true, 'Benötigte Konten (1400, 8000) vorhanden');
|
||||||
|
|
||||||
|
// Testkunden anlegen
|
||||||
|
$customerId = \Steuerkram\Models\Customer::create([
|
||||||
|
'nachname' => 'Buchungstest',
|
||||||
|
'firma' => 'Buchungs GmbH',
|
||||||
|
'strasse' => 'Hauptstraße 1',
|
||||||
|
'plz' => '10115',
|
||||||
|
'ort' => 'Berlin',
|
||||||
|
'payment_days' => 30,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Rechnung anlegen und finalisieren
|
||||||
|
$datum = date('Y-m-d');
|
||||||
|
$invoiceId = \Steuerkram\Models\Invoice::create([
|
||||||
|
'customer_id' => $customerId,
|
||||||
|
'betreff' => 'Testrechnung für Buchungstest',
|
||||||
|
'rechnungs_datum' => $datum,
|
||||||
|
'leistungszeitraum_von' => date('Y-m-01'),
|
||||||
|
'leistungszeitraum_bis' => date('Y-m-t'),
|
||||||
|
'lines' => [
|
||||||
|
[
|
||||||
|
'bezeichnung' => 'Beratungsleistung',
|
||||||
|
'menge' => 5,
|
||||||
|
'einheit' => 'Std.',
|
||||||
|
'einzelpreis' => 200.00,
|
||||||
|
'ust_satz' => 19.0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$nr = \Steuerkram\Models\Invoice::finalize($invoiceId);
|
||||||
|
$this->assertNotEmpty($nr, 'Rechnung wurde finalisiert (Rechnungsnummer erhalten)');
|
||||||
|
|
||||||
|
$invoice = \Steuerkram\Models\Invoice::find($invoiceId);
|
||||||
|
$this->assertEquals('Gesendet', $invoice['status'] ?? null, 'Rechnungsstatus ist Gesendet');
|
||||||
|
|
||||||
|
// Buchungen erzeugen (wie InvoiceController::createBelegAndBookings)
|
||||||
|
$belegNr = \Steuerkram\Models\Beleg::generateBelegNr();
|
||||||
|
$belegId = \Steuerkram\Models\Beleg::create([
|
||||||
|
'beleg_nr' => $belegNr,
|
||||||
|
'filename' => $nr . '.pdf',
|
||||||
|
'storage_path' => '',
|
||||||
|
'mime_type' => 'application/pdf',
|
||||||
|
'file_hash' => hash('sha256', $nr),
|
||||||
|
'beleg_datum' => $invoice['rechnungs_datum'],
|
||||||
|
'eingangs_datum' => $invoice['rechnungs_datum'],
|
||||||
|
'rechnungs_nr' => $nr,
|
||||||
|
'betrag_brutto' => $invoice['betrag_brutto'],
|
||||||
|
'belegtyp' => 'Ausgangsrechnung',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$lines = \Steuerkram\Models\Invoice::lines($invoiceId);
|
||||||
|
$gruppenNetto = [];
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$satz = (float)$line['ust_satz'];
|
||||||
|
$netto = round((float)$line['menge'] * (float)$line['einzelpreis'], 2);
|
||||||
|
$gruppenNetto[$satz] = ($gruppenNetto[$satz] ?? 0.0) + $netto;
|
||||||
|
}
|
||||||
|
|
||||||
|
$buchungIds = [];
|
||||||
|
foreach ($gruppenNetto as $satz => $netto) {
|
||||||
|
$buchungIds[] = \Steuerkram\Models\Booking::create([
|
||||||
|
'beleg_id' => $belegId,
|
||||||
|
'buchungs_datum' => $invoice['rechnungs_datum'],
|
||||||
|
'soll_konto_id' => $debitorId,
|
||||||
|
'haben_konto_id' => $erloesId,
|
||||||
|
'betrag_netto' => round($netto, 2),
|
||||||
|
'ust_satz' => $satz,
|
||||||
|
'buchungstext' => 'Ausgangsrechnung ' . $nr,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assertGreaterThan(0, count($buchungIds), 'Mindestens eine Buchung wurde erzeugt');
|
||||||
|
|
||||||
|
// Im Journal auffindbar
|
||||||
|
$year = (int)date('Y');
|
||||||
|
$month = (int)date('n');
|
||||||
|
$journal = \Steuerkram\Models\Booking::journal(['year' => $year, 'month' => $month], 1, 999);
|
||||||
|
|
||||||
|
$foundCount = 0;
|
||||||
|
foreach ($journal['rows'] as $row) {
|
||||||
|
if (in_array((int)$row['id'], $buchungIds, true)) {
|
||||||
|
$foundCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->assertEquals(count($buchungIds), $foundCount, 'Alle Buchungszeilen im Buchungsjournal');
|
||||||
|
|
||||||
|
// Buchungsinhalt prüfen (erste Buchung)
|
||||||
|
$firstId = $buchungIds[0];
|
||||||
|
foreach ($journal['rows'] as $row) {
|
||||||
|
if ((int)$row['id'] === $firstId) {
|
||||||
|
$this->assertEquals(1000.00, (float)$row['betrag_netto'], 'Nettobetrag 1000 € korrekt');
|
||||||
|
$this->assertEquals(19.0, (float)$row['ust_satz'], 'USt-Satz 19 % korrekt');
|
||||||
|
$this->assertEquals(190.00, (float)$row['betrag_ust'], 'USt-Betrag 190 € korrekt');
|
||||||
|
$this->assertEquals(1190.00, (float)$row['betrag_brutto'], 'Bruttobetrag 1190 € korrekt');
|
||||||
|
$this->assertNotEmpty($row['beleg_nr'], 'Belegnummer im Journal vorhanden');
|
||||||
|
$this->assertNotEmpty($row['soll_nr'], 'Soll-Konto im Journal vorhanden');
|
||||||
|
$this->assertNotEmpty($row['haben_nr'], 'Haben-Konto im Journal vorhanden');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
159
tests/05_TaxSummenTest.php
Normal file
159
tests/05_TaxSummenTest.php
Normal file
|
|
@ -0,0 +1,159 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test 5: Buchungen in UStVA und EÜR.
|
||||||
|
*
|
||||||
|
* Erstellt Buchungen mit bekannten Beträgen und prüft, ob
|
||||||
|
* UstvaCalculator und EuerCalculator die Werte korrekt summieren.
|
||||||
|
*/
|
||||||
|
class TaxSummenTest extends TestCase
|
||||||
|
{
|
||||||
|
protected string $testName = 'Test 5: Buchungen in UStVA und EÜR';
|
||||||
|
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$year = (int)date('Y');
|
||||||
|
$month = (int)date('n');
|
||||||
|
$datum = date('Y-m-d');
|
||||||
|
|
||||||
|
// Benötigte Konten (SKR03)
|
||||||
|
$debitorId = $this->findAccountId('1400'); // Forderungen (Aktiv)
|
||||||
|
$erloesId = $this->findAccountId('8000'); // Erlöse 19 % (Ertrag)
|
||||||
|
$aufwandId = $this->findAccountId('4920'); // Bürobedarf (Aufwand)
|
||||||
|
$bankId = $this->findAccountId('1200'); // Bank (Aktiv)
|
||||||
|
|
||||||
|
// Fallback auf beliebige Konten der passenden Art
|
||||||
|
if (!$erloesId) {
|
||||||
|
$row = \Steuerkram\Database::fetchOne(
|
||||||
|
"SELECT id FROM accounts WHERE kontenart = 'Ertrag' AND aktiv = 1 LIMIT 1"
|
||||||
|
);
|
||||||
|
$erloesId = $row ? (int)$row['id'] : null;
|
||||||
|
}
|
||||||
|
if (!$aufwandId) {
|
||||||
|
$row = \Steuerkram\Database::fetchOne(
|
||||||
|
"SELECT id FROM accounts WHERE kontenart = 'Aufwand' AND aktiv = 1 LIMIT 1"
|
||||||
|
);
|
||||||
|
$aufwandId = $row ? (int)$row['id'] : null;
|
||||||
|
}
|
||||||
|
if (!$debitorId) {
|
||||||
|
$row = \Steuerkram\Database::fetchOne(
|
||||||
|
"SELECT id FROM accounts WHERE kontenart = 'Aktiv' AND aktiv = 1 LIMIT 1"
|
||||||
|
);
|
||||||
|
$debitorId = $row ? (int)$row['id'] : null;
|
||||||
|
}
|
||||||
|
if (!$bankId) {
|
||||||
|
$row = \Steuerkram\Database::fetchOne(
|
||||||
|
"SELECT id FROM accounts WHERE kontenart = 'Aktiv' AND aktiv = 1 AND id != ? LIMIT 1",
|
||||||
|
[$debitorId ?? 0]
|
||||||
|
);
|
||||||
|
$bankId = $row ? (int)$row['id'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$debitorId || !$erloesId || !$aufwandId || !$bankId) {
|
||||||
|
$this->assert(false, 'Benötigte Konten nicht gefunden – SKR03-Seed vorhanden?');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->assert(true, 'Alle benötigten Konten gefunden');
|
||||||
|
|
||||||
|
// ── Ausgangsbuchung (Ertrag, 19 %) ────────────────────────────────
|
||||||
|
// Debitor an Erlös 19%: Netto=500, USt=95, Brutto=595
|
||||||
|
$belegAr = \Steuerkram\Models\Beleg::create([
|
||||||
|
'beleg_nr' => 'TEST-AR-' . uniqid(),
|
||||||
|
'filename' => 'test-ar.pdf',
|
||||||
|
'storage_path' => '',
|
||||||
|
'mime_type' => 'application/pdf',
|
||||||
|
'file_hash' => hash('sha256', 'test-ar'),
|
||||||
|
'beleg_datum' => $datum,
|
||||||
|
'eingangs_datum' => $datum,
|
||||||
|
'belegtyp' => 'Ausgangsrechnung',
|
||||||
|
]);
|
||||||
|
\Steuerkram\Models\Booking::create([
|
||||||
|
'beleg_id' => $belegAr,
|
||||||
|
'buchungs_datum' => $datum,
|
||||||
|
'soll_konto_id' => $debitorId,
|
||||||
|
'haben_konto_id' => $erloesId,
|
||||||
|
'betrag_netto' => 500.00,
|
||||||
|
'ust_satz' => 19.0,
|
||||||
|
'buchungstext' => 'Test Ausgangsbuchung USt 19%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── Eingangsbuchung (Aufwand, 19 %) ───────────────────────────────
|
||||||
|
// Aufwand an Bank: Netto=200, USt=38, Brutto=238
|
||||||
|
$belegEr = \Steuerkram\Models\Beleg::create([
|
||||||
|
'beleg_nr' => 'TEST-ER-' . uniqid(),
|
||||||
|
'filename' => 'test-er.pdf',
|
||||||
|
'storage_path' => 'static/uploads/test/test-er.pdf',
|
||||||
|
'mime_type' => 'application/pdf',
|
||||||
|
'file_hash' => hash('sha256', 'test-er'),
|
||||||
|
'beleg_datum' => $datum,
|
||||||
|
'eingangs_datum' => $datum,
|
||||||
|
'belegtyp' => 'Eingangsrechnung',
|
||||||
|
]);
|
||||||
|
\Steuerkram\Models\Booking::create([
|
||||||
|
'beleg_id' => $belegEr,
|
||||||
|
'buchungs_datum' => $datum,
|
||||||
|
'soll_konto_id' => $aufwandId,
|
||||||
|
'haben_konto_id' => $bankId,
|
||||||
|
'betrag_netto' => 200.00,
|
||||||
|
'ust_satz' => 19.0,
|
||||||
|
'buchungstext' => 'Test Eingangsbuchung USt 19%',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ── UStVA prüfen ──────────────────────────────────────────────────
|
||||||
|
$calc = new \Steuerkram\Services\UstvaCalculator();
|
||||||
|
$kz = $calc->calculate($year, $month);
|
||||||
|
|
||||||
|
// Kz 81: Steuerpfl. Umsätze 19% – muss mindestens 500 € enthalten
|
||||||
|
$this->assert(
|
||||||
|
(float)$kz['81']['betrag'] >= 500.00,
|
||||||
|
'UStVA Kz 81 (Umsätze 19%): Betrag enthält Ausgangsbuchung ≥ 500 €'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Kz 83: USt auf Umsätze 19% – abgeleitet (19% von Kz81-Betrag)
|
||||||
|
$this->assert(
|
||||||
|
(float)$kz['83']['steuer'] >= 95.00,
|
||||||
|
'UStVA Kz 83 (USt 19%): Steuerbetrag ≥ 95 €'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Kz 61: Vorsteuer 19% – muss mindestens 38 € enthalten
|
||||||
|
$this->assert(
|
||||||
|
(float)$kz['61']['steuer'] >= 38.00,
|
||||||
|
'UStVA Kz 61 (Vorsteuer 19%): Steuerbetrag enthält Eingangsbuchung ≥ 38 €'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Kz 66: Vorsteuer gesamt – muss ≥ Kz 61 sein
|
||||||
|
$this->assert(
|
||||||
|
(float)$kz['66']['steuer'] >= (float)$kz['61']['steuer'],
|
||||||
|
'UStVA Kz 66 (Vorsteuer gesamt): ≥ Kz 61'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Kz 69 Berechnung: USt gesamt - Vorsteuer gesamt
|
||||||
|
$erwKz69 = round(
|
||||||
|
((float)$kz['83']['steuer'] + (float)$kz['84']['steuer']) - (float)$kz['66']['steuer'],
|
||||||
|
2
|
||||||
|
);
|
||||||
|
$this->assertEquals($erwKz69, (float)$kz['69']['steuer'], 'UStVA Kz 69 (Zahllast) korrekt berechnet');
|
||||||
|
|
||||||
|
// ── EÜR prüfen ────────────────────────────────────────────────────
|
||||||
|
$euerCalc = new \Steuerkram\Services\EuerCalculator();
|
||||||
|
$euer = $euerCalc->calculate($year);
|
||||||
|
|
||||||
|
// Einnahmen müssen vorhanden sein
|
||||||
|
$this->assert(
|
||||||
|
(float)$euer['total_einnahmen'] >= 500.00,
|
||||||
|
'EÜR: Einnahmen ≥ 500 € (Ausgangsbuchung eingeflossen)'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ausgaben müssen vorhanden sein
|
||||||
|
$this->assert(
|
||||||
|
(float)$euer['total_ausgaben'] >= 200.00,
|
||||||
|
'EÜR: Ausgaben ≥ 200 € (Eingangsbuchung eingeflossen)'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Gewinn = Einnahmen - Ausgaben
|
||||||
|
$erwGewinn = round((float)$euer['total_einnahmen'] - (float)$euer['total_ausgaben'], 2);
|
||||||
|
$this->assertEquals($erwGewinn, (float)$euer['gewinn'], 'EÜR: Gewinn = Einnahmen - Ausgaben');
|
||||||
|
}
|
||||||
|
}
|
||||||
115
tests/TestCase.php
Normal file
115
tests/TestCase.php
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Basis-Testklasse für Steuerkram-Tests.
|
||||||
|
*
|
||||||
|
* Jeder Test läuft in einer MySQL-Transaktion, die am Ende zurückgerollt wird.
|
||||||
|
* Damit bleibt die Datenbank nach dem Test unverändert.
|
||||||
|
*/
|
||||||
|
abstract class TestCase
|
||||||
|
{
|
||||||
|
private static int $totalPassed = 0;
|
||||||
|
private static int $totalFailed = 0;
|
||||||
|
private static array $allFailures = [];
|
||||||
|
|
||||||
|
protected int $passed = 0;
|
||||||
|
protected int $failed = 0;
|
||||||
|
protected array $failures = [];
|
||||||
|
protected string $testName = '';
|
||||||
|
|
||||||
|
abstract public function run(): void;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
\Steuerkram\Database::beginTransaction();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
\Steuerkram\Database::rollBack();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// Transaktion war evtl. nicht aktiv
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assert(bool $condition, string $message): void
|
||||||
|
{
|
||||||
|
if ($condition) {
|
||||||
|
$this->passed++;
|
||||||
|
echo " \e[32m✓\e[0m {$message}\n";
|
||||||
|
} else {
|
||||||
|
$this->failed++;
|
||||||
|
$this->failures[] = "[{$this->testName}] {$message}";
|
||||||
|
echo " \e[31m✗\e[0m {$message}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertEquals(mixed $expected, mixed $actual, string $message): void
|
||||||
|
{
|
||||||
|
$this->assert(
|
||||||
|
$expected == $actual,
|
||||||
|
"{$message} (erwartet: " . json_encode($expected) . ", erhalten: " . json_encode($actual) . ")"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertGreaterThan(int $min, mixed $actual, string $message): void
|
||||||
|
{
|
||||||
|
$this->assert($actual > $min, "{$message} (Wert: {$actual}, muss > {$min})");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertNotEmpty(mixed $value, string $message): void
|
||||||
|
{
|
||||||
|
$this->assert(!empty($value), $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Liefert Konto-ID anhand der Kontonummer oder null wenn nicht gefunden.
|
||||||
|
*/
|
||||||
|
protected function findAccountId(string $kontonummer): ?int
|
||||||
|
{
|
||||||
|
$row = \Steuerkram\Database::fetchOne(
|
||||||
|
'SELECT id FROM accounts WHERE kontonummer = ?',
|
||||||
|
[$kontonummer]
|
||||||
|
);
|
||||||
|
return $row ? (int)$row['id'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function execute(): array
|
||||||
|
{
|
||||||
|
echo "\n\e[1m{$this->testName}\e[0m\n";
|
||||||
|
$this->setUp();
|
||||||
|
try {
|
||||||
|
$this->run();
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
$this->failed++;
|
||||||
|
$msg = "[{$this->testName}] Ausnahme: " . $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
|
||||||
|
$this->failures[] = $msg;
|
||||||
|
echo " \e[31m✗\e[0m Unerwartete Ausnahme: " . $e->getMessage() . "\n";
|
||||||
|
} finally {
|
||||||
|
$this->tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = $this->failed === 0
|
||||||
|
? "\e[32mBestanden ({$this->passed}/{$this->passed})\e[0m"
|
||||||
|
: "\e[31m{$this->failed} Fehler, {$this->passed} OK\e[0m";
|
||||||
|
echo " → {$status}\n";
|
||||||
|
|
||||||
|
self::$totalPassed += $this->passed;
|
||||||
|
self::$totalFailed += $this->failed;
|
||||||
|
self::$allFailures = array_merge(self::$allFailures, $this->failures);
|
||||||
|
|
||||||
|
return $this->failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function globalSummary(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'passed' => self::$totalPassed,
|
||||||
|
'failed' => self::$totalFailed,
|
||||||
|
'failures' => self::$allFailures,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
90
tests/run.php
Normal file
90
tests/run.php
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
#!/usr/bin/env php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Steuerkram Test-Runner
|
||||||
|
*
|
||||||
|
* Führt alle Tests im tests/-Verzeichnis aus und gibt eine Zusammenfassung aus.
|
||||||
|
* Exit-Code: 0 = alle Tests bestanden, 1 = mindestens ein Fehler.
|
||||||
|
*
|
||||||
|
* Verwendung:
|
||||||
|
* php tests/run.php
|
||||||
|
* php tests/run.php --json # Ausgabe als JSON (für Skript-Auswertung)
|
||||||
|
*/
|
||||||
|
|
||||||
|
$projectRoot = dirname(__DIR__);
|
||||||
|
$autoload = $projectRoot . '/vendor/autoload.php';
|
||||||
|
|
||||||
|
if (!file_exists($autoload)) {
|
||||||
|
fwrite(STDERR, "Fehler: vendor/autoload.php nicht gefunden. Bitte 'composer install' ausführen.\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
require $autoload;
|
||||||
|
require __DIR__ . '/TestCase.php';
|
||||||
|
|
||||||
|
\Steuerkram\Config::load();
|
||||||
|
|
||||||
|
$jsonMode = in_array('--json', $argv ?? [], true);
|
||||||
|
|
||||||
|
// Alle Test-Dateien laden
|
||||||
|
$testFiles = glob(__DIR__ . '/[0-9][0-9]_*.php');
|
||||||
|
sort($testFiles);
|
||||||
|
|
||||||
|
$testClasses = [];
|
||||||
|
foreach ($testFiles as $file) {
|
||||||
|
require_once $file;
|
||||||
|
// Klassenname aus Dateiname ableiten: 01_BelegUploadTest.php → BelegUploadTest
|
||||||
|
$base = basename($file, '.php');
|
||||||
|
$className = preg_replace('/^\d+_/', '', $base);
|
||||||
|
if (class_exists($className)) {
|
||||||
|
$testClasses[] = $className;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($testClasses)) {
|
||||||
|
fwrite(STDERR, "Keine Testklassen gefunden.\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$jsonMode) {
|
||||||
|
echo "\n\e[1m╔══════════════════════════════════════════╗\e[0m\n";
|
||||||
|
echo "\e[1m║ Steuerkram Test-Suite ║\e[0m\n";
|
||||||
|
echo "\e[1m╚══════════════════════════════════════════╝\e[0m\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$allFailures = [];
|
||||||
|
|
||||||
|
foreach ($testClasses as $class) {
|
||||||
|
/** @var TestCase $test */
|
||||||
|
$test = new $class();
|
||||||
|
$failures = $test->execute();
|
||||||
|
$allFailures = array_merge($allFailures, $failures);
|
||||||
|
}
|
||||||
|
|
||||||
|
$summary = TestCase::globalSummary();
|
||||||
|
|
||||||
|
if (!$jsonMode) {
|
||||||
|
$total = $summary['passed'] + $summary['failed'];
|
||||||
|
echo "\n\e[1m══════════════════════════════════════════\e[0m\n";
|
||||||
|
if ($summary['failed'] === 0) {
|
||||||
|
echo "\e[32;1m✓ Alle {$summary['passed']} Tests bestanden.\e[0m\n";
|
||||||
|
} else {
|
||||||
|
echo "\e[31;1m✗ {$summary['failed']} von {$total} Tests fehlgeschlagen:\e[0m\n";
|
||||||
|
foreach ($summary['failures'] as $failure) {
|
||||||
|
echo " • {$failure}\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
echo "\e[1m══════════════════════════════════════════\e[0m\n\n";
|
||||||
|
} else {
|
||||||
|
echo json_encode([
|
||||||
|
'passed' => $summary['passed'],
|
||||||
|
'failed' => $summary['failed'],
|
||||||
|
'failures' => $summary['failures'],
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
|
||||||
|
echo "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
exit($summary['failed'] > 0 ? 1 : 0);
|
||||||
Loading…
Add table
Reference in a new issue