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>
583 lines
19 KiB
PHP
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.'));
|
|
}
|
|
}
|