FSCash/tests/FscashTest.php
fds2610-bot 03a6cf774d Feature: lesende API für den Buchungsabgleich (Issue #13)
Neue, rein lesende HTTP-Schnittstelle für den Finanzmanager:
GET /api/v1/buchungen sucht Buchungen anhand Zeitraum, Betrag (mit
Toleranz) und Währung; GET /api/v1/status dient als Verbindungstest.
Beide sind per HTTP Basic Auth mit den bestehenden FSCash-Zugangsdaten
geschützt und rechnen die intern positiv geführten Bargeldausgaben in
die vom Finanzmanager erwartete Vorzeichenkonvention (Ausgaben negativ)
um.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-19 07:31:31 +02:00

583 lines
19 KiB
PHP

<?php
declare(strict_types=1);
use PHPUnit\Framework\TestCase;
/**
* Tests für die reinen Hilfsfunktionen in html/lib/fscash.php.
*
* Diese Funktionen bilden die wichtigste Geschäftslogik von FSCash ab:
* Monatsnavigation, Zugriffsrechte, Aktien-Gewinn/Verlust-Berechnung,
* Auswertung externer API-Antworten, Eingabevalidierung, CSV-Export und
* das Migrationssystem.
*/
final class FscashTest extends TestCase
{
// --- fscash_normalize_month_year ---------------------------------
public function testNormalizeMonthYearLaesstGueltigenMonatUnveraendert(): void
{
$this->assertSame(
['month' => 6, 'year' => 2026],
fscash_normalize_month_year(6, 2026)
);
}
public function testNormalizeMonthYearSpringtBeiUnterschreitungInsVorjahr(): void
{
$this->assertSame(
['month' => 12, 'year' => 2025],
fscash_normalize_month_year(0, 2026)
);
}
public function testNormalizeMonthYearSpringtBeiUeberschreitungInsFolgejahr(): void
{
$this->assertSame(
['month' => 1, 'year' => 2027],
fscash_normalize_month_year(13, 2026)
);
}
public function testNormalizeMonthYearGrenzwerteBleibenUnveraendert(): void
{
$this->assertSame(['month' => 1, 'year' => 2026], fscash_normalize_month_year(1, 2026));
$this->assertSame(['month' => 12, 'year' => 2026], fscash_normalize_month_year(12, 2026));
}
// --- fscash_user_roles --------------------------------------------
public function testUserRolesErkenntAdminUndAktienUser(): void
{
$this->assertSame(
['is_admin' => true, 'is_aktien' => true],
fscash_user_roles(1, [1, 2], [1, 2])
);
}
public function testUserRolesErkenntNormalenUserOhneRechte(): void
{
$this->assertSame(
['is_admin' => false, 'is_aktien' => false],
fscash_user_roles(99, [1, 2], [1, 2])
);
}
public function testUserRolesVergleichtStriktTypisiert(): void
{
// "1" (string) darf ein int 1 in der Liste nicht faelschlich matchen,
// da fscash_user_roles ausschliesslich mit int-typisierten IDs arbeitet
// und in_array(..., true) strikt vergleicht.
$this->assertSame(
['is_admin' => true, 'is_aktien' => false],
fscash_user_roles(2, [1, 2], [1])
);
}
// --- fscash_aktie_guv ------------------------------------------------
public function testAktieGuvBerechnetGewinn(): void
{
$result = fscash_aktie_guv(100.0, 10.0, 120.0);
$this->assertSame(1000.0, $result['kaufSumme']);
$this->assertSame(1200.0, $result['aktuellerWert']);
$this->assertSame(200.0, $result['guv']);
$this->assertSame('guv-positive', $result['guvClass']);
}
public function testAktieGuvBerechnetVerlust(): void
{
$result = fscash_aktie_guv(100.0, 10.0, 80.0);
$this->assertSame(-200.0, $result['guv']);
$this->assertSame('guv-negative', $result['guvClass']);
}
public function testAktieGuvBeiGleichbleibendemKursOhneKlasse(): void
{
$result = fscash_aktie_guv(50.0, 4.0, 50.0);
$this->assertSame(0.0, $result['guv']);
$this->assertSame('', $result['guvClass']);
}
public function testAktieGuvOhneKursErgibtNegativenGuvGleichKaufsumme(): void
{
// kurs = 0 (noch keine Kursaktualisierung erfolgt)
$result = fscash_aktie_guv(100.0, 3.0, 0.0);
$this->assertSame(300.0, $result['kaufSumme']);
$this->assertSame(0.0, $result['aktuellerWert']);
$this->assertSame(-300.0, $result['guv']);
$this->assertSame('guv-negative', $result['guvClass']);
}
// --- fscash_select_figi_symbol --------------------------------------
public function testSelectFigiSymbolBevorzugtXetraEintrag(): void
{
$entries = [
['ticker' => 'FOO', 'exchCode' => 'US'],
['ticker' => 'BAR', 'exchCode' => 'XETR'],
];
$this->assertSame('BAR.XETR', fscash_select_figi_symbol($entries));
}
public function testSelectFigiSymbolNimmtErstenEintragOhneXetra(): void
{
$entries = [
['ticker' => 'FOO', 'exchCode' => 'US'],
['ticker' => 'BAZ', 'exchCode' => 'LSE'],
];
$this->assertSame('FOO.US', fscash_select_figi_symbol($entries));
}
public function testSelectFigiSymbolOhneExchCode(): void
{
$entries = [
['ticker' => 'FOO'],
];
$this->assertSame('FOO', fscash_select_figi_symbol($entries));
}
public function testSelectFigiSymbolBeiLeeremTickerLiefertNull(): void
{
$entries = [
['ticker' => '', 'exchCode' => 'US'],
];
$this->assertNull(fscash_select_figi_symbol($entries));
}
public function testSelectFigiSymbolBeiLeererListeLiefertNull(): void
{
$this->assertNull(fscash_select_figi_symbol([]));
}
// --- fscash_resolve_selected_categories ------------------------------
public function testResolveSelectedCategoriesMitAuswahl(): void
{
$this->assertSame(
[2, 3],
fscash_resolve_selected_categories([1, 2, 3, 4], [2, 3])
);
}
public function testResolveSelectedCategoriesOhneAuswahlLiefertAlle(): void
{
$this->assertSame(
[1, 2, 3, 4],
fscash_resolve_selected_categories([1, 2, 3, 4], null)
);
}
public function testResolveSelectedCategoriesBeiLeeremArrayLiefertAlle(): void
{
// Ein leeres categories[]-Array (z.B. alle Haken entfernt) fällt
// bewusst auf "alle Kategorien" zurück, siehe rep-person.php.
$this->assertSame(
[1, 2, 3, 4],
fscash_resolve_selected_categories([1, 2, 3, 4], [])
);
}
// --- fscash_resolve_selected_payment_methods --------------------------
public function testResolveSelectedPaymentMethodsMitAuswahl(): void
{
$this->assertSame(
[2, 3],
fscash_resolve_selected_payment_methods([1, 2, 3, 4], [2, 3])
);
}
public function testResolveSelectedPaymentMethodsOhneAuswahlLiefertAlle(): void
{
$this->assertSame(
[1, 2, 3, 4],
fscash_resolve_selected_payment_methods([1, 2, 3, 4], null)
);
}
public function testResolveSelectedPaymentMethodsBeiLeeremArrayLiefertAlle(): void
{
$this->assertSame(
[1, 2, 3, 4],
fscash_resolve_selected_payment_methods([1, 2, 3, 4], [])
);
}
// --- fscash_extract_quote_price --------------------------------------
public function testExtractQuotePriceLiestPreisAus(): void
{
$response = ['Global Quote' => ['05. price' => '123.4500']];
$this->assertSame(123.45, fscash_extract_quote_price($response));
}
public function testExtractQuotePriceBeiFehlendemFeldLiefertNull(): void
{
$this->assertNull(fscash_extract_quote_price(['Global Quote' => []]));
$this->assertNull(fscash_extract_quote_price([]));
}
public function testExtractQuotePriceBeiLeeremStringLiefertNull(): void
{
$this->assertNull(fscash_extract_quote_price(['Global Quote' => ['05. price' => '']]));
}
// --- fscash_map_symbol_search_results ---------------------------------
public function testMapSymbolSearchResultsWandeltAlphaVantageFormatUm(): void
{
$response = [
'bestMatches' => [
['1. symbol' => 'SAP.DE', '2. name' => 'SAP SE', '4. region' => 'Frankfurt'],
],
];
$this->assertSame(
[['symbol' => 'SAP.DE', 'name' => 'SAP SE', 'region' => 'Frankfurt']],
fscash_map_symbol_search_results($response)
);
}
public function testMapSymbolSearchResultsOhneTrefferLiefertLeeresArray(): void
{
$this->assertSame([], fscash_map_symbol_search_results([]));
$this->assertSame([], fscash_map_symbol_search_results(['bestMatches' => []]));
}
// --- fscash_is_valid_symbol_payload -----------------------------------
public function testIsValidSymbolPayloadMitGueltigenWerten(): void
{
$this->assertTrue(fscash_is_valid_symbol_payload(42, 'SAP.DE'));
$this->assertTrue(fscash_is_valid_symbol_payload('42', 'SAP.DE'));
}
public function testIsValidSymbolPayloadOhneIdOderSymbolIstUngueltig(): void
{
$this->assertFalse(fscash_is_valid_symbol_payload(null, 'SAP.DE'));
$this->assertFalse(fscash_is_valid_symbol_payload(42, null));
$this->assertFalse(fscash_is_valid_symbol_payload(0, 'SAP.DE'));
$this->assertFalse(fscash_is_valid_symbol_payload(42, ''));
}
// --- fscash_format_report_month_csv_row --------------------------------
public function testFormatReportMonthCsvRow(): void
{
$entry = [
'id' => 5,
'datum' => '2026-07-01',
'benutzer_name' => 'Alice',
'kategorie_id' => 3,
'kategorie_name' => 'Einkaufen',
'betreff' => 'Wocheneinkauf',
'betrag' => 1234.5,
'zahlungsmethode' => 'Karte',
];
$this->assertSame(
['5', '2026-07-01', 'Alice', '3', 'Einkaufen', 'Wocheneinkauf', '1.234,50', 'Karte'],
fscash_format_report_month_csv_row($entry)
);
}
public function testFormatReportMonthCsvRowMitFehlendenFeldern(): void
{
$this->assertSame(
['', '', '', '', '', '', '0,00', ''],
fscash_format_report_month_csv_row([])
);
}
// --- fscash_pending_migrations ------------------------------------------
public function testPendingMigrationsFiltertBereitsAngewendete(): void
{
$available = ['/m/0002_add_col.sql', '/m/0001_init.sql', '/m/0003_index.sql'];
$applied = ['0001_init.sql'];
$this->assertSame(
['/m/0002_add_col.sql', '/m/0003_index.sql'],
fscash_pending_migrations($available, $applied)
);
}
public function testPendingMigrationsSortiertNumerischAlsString(): void
{
$available = ['/m/0010_b.sql', '/m/0002_a.sql'];
$this->assertSame(
['/m/0002_a.sql', '/m/0010_b.sql'],
fscash_pending_migrations($available, [])
);
}
public function testPendingMigrationsBeiAllenAngewendetLiefertLeeresArray(): void
{
$available = ['/m/0001_init.sql'];
$applied = ['0001_init.sql'];
$this->assertSame([], fscash_pending_migrations($available, $applied));
}
// --- fscash_parse_basic_auth_header --------------------------------
public function testParseBasicAuthHeaderMitGueltigenZugangsdaten(): void
{
$header = 'Basic ' . base64_encode('finanzmanager:geheim123');
$this->assertSame(
['username' => 'finanzmanager', 'password' => 'geheim123'],
fscash_parse_basic_auth_header($header)
);
}
public function testParseBasicAuthHeaderErlaubtDoppelpunktImPasswort(): void
{
$header = 'Basic ' . base64_encode('user:pass:mit:doppelpunkt');
$this->assertSame(
['username' => 'user', 'password' => 'pass:mit:doppelpunkt'],
fscash_parse_basic_auth_header($header)
);
}
public function testParseBasicAuthHeaderOhneHeaderLiefertNull(): void
{
$this->assertNull(fscash_parse_basic_auth_header(null));
}
public function testParseBasicAuthHeaderOhneBasicSchemaLiefertNull(): void
{
$this->assertNull(fscash_parse_basic_auth_header('Bearer irgendwas'));
}
public function testParseBasicAuthHeaderMitUngueltigemBase64LiefertNull(): void
{
$this->assertNull(fscash_parse_basic_auth_header('Basic ###nicht-base64###'));
}
public function testParseBasicAuthHeaderOhneDoppelpunktLiefertNull(): void
{
$this->assertNull(fscash_parse_basic_auth_header('Basic ' . base64_encode('ohnedoppelpunkt')));
}
// --- fscash_is_valid_date -------------------------------------------
public function testIsValidDateMitGueltigemDatum(): void
{
$this->assertTrue(fscash_is_valid_date('2026-08-19'));
$this->assertTrue(fscash_is_valid_date('2024-02-29'));
}
public function testIsValidDateMitUngueltigemDatumOderFormat(): void
{
$this->assertFalse(fscash_is_valid_date('2026-02-30'));
$this->assertFalse(fscash_is_valid_date('2023-02-29'));
$this->assertFalse(fscash_is_valid_date('19.08.2026'));
$this->assertFalse(fscash_is_valid_date('2026-8-19'));
$this->assertFalse(fscash_is_valid_date(''));
}
// --- fscash_validate_buchungen_query ---------------------------------
public function testValidateBuchungenQueryMitPflichtfeldern(): void
{
$result = fscash_validate_buchungen_query([
'von' => '2026-08-09',
'bis' => '2026-08-19',
'betrag' => '-12.50',
]);
$this->assertNull($result['error']);
$this->assertSame('2026-08-09', $result['von']);
$this->assertSame('2026-08-19', $result['bis']);
$this->assertSame(-12.50, $result['betrag']);
$this->assertSame(0.0, $result['toleranz']);
$this->assertSame('EUR', $result['waehrung']);
$this->assertSame(20, $result['limit']);
}
public function testValidateBuchungenQueryMitAllenOptionalenParametern(): void
{
$result = fscash_validate_buchungen_query([
'von' => '2026-08-09',
'bis' => '2026-08-19',
'betrag' => '-12.50',
'toleranz' => '0.50',
'waehrung' => 'eur',
'limit' => '5',
]);
$this->assertNull($result['error']);
$this->assertSame(0.50, $result['toleranz']);
$this->assertSame('EUR', $result['waehrung']);
$this->assertSame(5, $result['limit']);
}
public function testValidateBuchungenQueryOhneVonLiefertFehler(): void
{
$result = fscash_validate_buchungen_query(['bis' => '2026-08-19', 'betrag' => '-12.50']);
$this->assertSame("Parameter 'von' fehlt.", $result['error']);
}
public function testValidateBuchungenQueryOhneBisLiefertFehler(): void
{
$result = fscash_validate_buchungen_query(['von' => '2026-08-09', 'betrag' => '-12.50']);
$this->assertSame("Parameter 'bis' fehlt.", $result['error']);
}
public function testValidateBuchungenQueryMitUngueltigemDatumLiefertFehler(): void
{
$result = fscash_validate_buchungen_query([
'von' => '2026-08-32',
'bis' => '2026-08-19',
'betrag' => '-12.50',
]);
$this->assertSame("Parameter 'von' ist unbrauchbar.", $result['error']);
}
public function testValidateBuchungenQueryMitVonNachBisLiefertFehler(): void
{
$result = fscash_validate_buchungen_query([
'von' => '2026-08-19',
'bis' => '2026-08-09',
'betrag' => '-12.50',
]);
$this->assertSame("Parameter 'von' darf nicht nach 'bis' liegen.", $result['error']);
}
public function testValidateBuchungenQueryOhneBetragLiefertFehler(): void
{
$result = fscash_validate_buchungen_query(['von' => '2026-08-09', 'bis' => '2026-08-19']);
$this->assertSame("Parameter 'betrag' fehlt.", $result['error']);
}
public function testValidateBuchungenQueryMitUnbrauchbaremBetragLiefertFehler(): void
{
$result = fscash_validate_buchungen_query([
'von' => '2026-08-09',
'bis' => '2026-08-19',
'betrag' => 'zwölffünfzig',
]);
$this->assertSame("Parameter 'betrag' ist unbrauchbar.", $result['error']);
}
public function testValidateBuchungenQueryMitNegativerToleranzLiefertFehler(): void
{
$result = fscash_validate_buchungen_query([
'von' => '2026-08-09',
'bis' => '2026-08-19',
'betrag' => '-12.50',
'toleranz' => '-1',
]);
$this->assertSame("Parameter 'toleranz' ist unbrauchbar.", $result['error']);
}
public function testValidateBuchungenQueryMitUngueltigerWaehrungLiefertFehler(): void
{
$result = fscash_validate_buchungen_query([
'von' => '2026-08-09',
'bis' => '2026-08-19',
'betrag' => '-12.50',
'waehrung' => 'EURO',
]);
$this->assertSame("Parameter 'waehrung' ist unbrauchbar.", $result['error']);
}
public function testValidateBuchungenQueryMitUngueltigemLimitLiefertFehler(): void
{
$result = fscash_validate_buchungen_query([
'von' => '2026-08-09',
'bis' => '2026-08-19',
'betrag' => '-12.50',
'limit' => '0',
]);
$this->assertSame("Parameter 'limit' ist unbrauchbar.", $result['error']);
}
// --- fscash_signed_betrag ---------------------------------------------
public function testSignedBetragDrehtVorzeichenVonGespeicherterAusgabe(): void
{
$this->assertSame(-12.50, fscash_signed_betrag(12.50));
$this->assertSame(0.0, fscash_signed_betrag(0.0));
}
// --- fscash_format_buchung ---------------------------------------------
public function testFormatBuchungMitAllenFeldern(): void
{
$row = [
'id' => 42,
'datum' => '2026-08-14',
'betrag' => 12.50,
'zweck' => 'Wocheneinkauf',
'kategorie' => 'Lebensmittel',
'konto' => 'Portemonnaie',
];
$this->assertSame(
[
'id' => '42',
'datum' => '2026-08-14',
'betrag' => -12.50,
'waehrung' => 'EUR',
'zweck' => 'Wocheneinkauf',
'kategorie' => 'Lebensmittel',
'konto' => 'Portemonnaie',
],
fscash_format_buchung($row)
);
}
public function testFormatBuchungOhneKategorieUndKontoLaesstFelderWeg(): void
{
$row = [
'id' => 42,
'datum' => '2026-08-14',
'betrag' => 12.50,
'zweck' => 'Wocheneinkauf',
'kategorie' => null,
'konto' => null,
];
$this->assertSame(
[
'id' => '42',
'datum' => '2026-08-14',
'betrag' => -12.50,
'waehrung' => 'EUR',
'zweck' => 'Wocheneinkauf',
],
fscash_format_buchung($row)
);
}
// --- fscash_api_error ---------------------------------------------------
public function testApiErrorBautFehlerobjekt(): void
{
$this->assertSame(['fehler' => 'Parameter fehlt.'], fscash_api_error('Parameter fehlt.'));
}
}