Merge pull request 'V.2.2: Unit-Tests für wichtige Funktionen, automatisch geprüft bei Update' (#4) from fds2610-bot/FSCash:main into main
Reviewed-on: https://codeberg.org/fds2610/FSCash/pulls/4
This commit is contained in:
commit
637fa6301b
18 changed files with 2339 additions and 121 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -6,4 +6,6 @@ config.php
|
||||||
html/.Trash-1000
|
html/.Trash-1000
|
||||||
Dockerfile
|
Dockerfile
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
|
vendor/
|
||||||
|
.phpunit.result.cache
|
||||||
|
|
||||||
|
|
|
||||||
27
README.md
27
README.md
|
|
@ -8,11 +8,15 @@ Dokumentiere Ausgaben on-the-go als Webapp
|
||||||
- [ ] Zusammenfassen von mehreren Einträgen zu einer Aktie
|
- [ ] Zusammenfassen von mehreren Einträgen zu einer Aktie
|
||||||
- [ ] Prüfen ob APIKEYS leer sind und ggfs die Aktualisierung ausblenden
|
- [ ] Prüfen ob APIKEYS leer sind und ggfs die Aktualisierung ausblenden
|
||||||
|
|
||||||
|
## V.2.2
|
||||||
|
|
||||||
|
- [X] Unit-Tests (PHPUnit) für die wichtigsten Funktionen, gebündelt in `html/lib/fscash.php`: Monatsnavigation, Admin-/Aktien-Rollenprüfung, Aktien-GuV-Berechnung, OpenFIGI-/Alpha-Vantage-Antwortauswertung, Eingabevalidierung, CSV-Export und Migrations-Logik (siehe Abschnitt "Tests" unten).
|
||||||
|
- [X] `update.sh` führt die Tests jetzt automatisch vor jedem Deploy aus (inkl. `composer install`, falls nötig) und bricht bei Fehlschlag ab, bevor irgendetwas kopiert oder migriert wird.
|
||||||
|
|
||||||
## V.2.1
|
## V.2.1
|
||||||
|
|
||||||
- [X] `update.sh` zum Deployen auf einen Zielserver: prüft PHP-Syntax, kopiert den aktuellen git-Stand ins Webroot (ohne die produktive `config.php` zu überschreiben) und wendet danach ausstehende Datenbank-Migrationen an.
|
- [X] `update.sh` zum Deployen auf einen Zielserver: prüft PHP-Syntax, kopiert den aktuellen git-Stand ins Webroot (ohne die produktive `config.php` zu überschreiben) und wendet danach ausstehende Datenbank-Migrationen an.
|
||||||
- [X] neues Verzeichnis `migrations/` mit versioniertem Migrationssystem: nummerierte `.sql`-Dateien werden über eine `schema_migrations`-Tabelle nachverfolgt und jeweils nur einmal angewendet (siehe `migrations/README.md` für die Konvention).
|
- [X] neues Verzeichnis `migrations/` mit versioniertem Migrationssystem: nummerierte `.sql`-Dateien werden über eine `schema_migrations`-Tabelle nachverfolgt und jeweils nur einmal angewendet (siehe `migrations/README.md` für die Konvention).
|
||||||
- [X] Platzhalter in `update.sh` für einen automatischen Testlauf, sobald Tests existieren.
|
|
||||||
|
|
||||||
## V.2.0
|
## V.2.0
|
||||||
|
|
||||||
|
|
@ -107,3 +111,24 @@ Ausserdem solltet Ihr User registrieren und ggfs. die Kategorien anpassen.
|
||||||
|
|
||||||
Das sollte alles sein. Bitte erstellt ein Bug-Ticket, wenn ich etwas übersehen habe. Gerne könnt Ihr auch Pull-Requests erstellen.
|
Das sollte alles sein. Bitte erstellt ein Bug-Ticket, wenn ich etwas übersehen habe. Gerne könnt Ihr auch Pull-Requests erstellen.
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
|
||||||
|
Die wichtigsten, DB- und netzwerkfreien Funktionen liegen gebündelt in `html/lib/fscash.php`
|
||||||
|
(Monatsnavigation, Rollenprüfung, Aktien-GuV-Berechnung, Auswertung der OpenFIGI-/Alpha-Vantage-Antworten,
|
||||||
|
Eingabevalidierung, CSV-Export, Migrations-Logik) und werden über PHPUnit in `tests/FscashTest.php` geprüft.
|
||||||
|
|
||||||
|
Einmalig Abhängigkeiten installieren:
|
||||||
|
|
||||||
|
```
|
||||||
|
composer install
|
||||||
|
```
|
||||||
|
|
||||||
|
Tests lokal ausführen:
|
||||||
|
|
||||||
|
```
|
||||||
|
bash tests/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
`update.sh` führt diesen Schritt automatisch vor jedem Deploy aus (inkl. `composer install`, falls `vendor/`
|
||||||
|
noch fehlt) und bricht bei einem Testfehlschlag ab, bevor irgendetwas kopiert oder migriert wird.
|
||||||
|
|
||||||
|
|
|
||||||
20
composer.json
Normal file
20
composer.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "fds/fscash",
|
||||||
|
"description": "FSCash - Haushaltsausgaben & Aktiendepot",
|
||||||
|
"type": "project",
|
||||||
|
"license": "proprietary",
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^10.5"
|
||||||
|
},
|
||||||
|
"autoload-dev": {
|
||||||
|
"files": [
|
||||||
|
"html/lib/fscash.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "phpunit"
|
||||||
|
}
|
||||||
|
}
|
||||||
1673
composer.lock
generated
Normal file
1673
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require 'config.php';
|
||||||
|
require_once 'lib/fscash.php';
|
||||||
error_log('Session user_id: ' . (isset($_SESSION['user_id']) ? $_SESSION['user_id'] : 'nicht gesetzt'));
|
error_log('Session user_id: ' . (isset($_SESSION['user_id']) ? $_SESSION['user_id'] : 'nicht gesetzt'));
|
||||||
// 1. Session-Prüfung (nur für geschützte Seiten)
|
// 1. Session-Prüfung (nur für geschützte Seiten)
|
||||||
if (!isset($_SESSION['user_id'])) {
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
|
@ -11,12 +12,12 @@ if (!isset($_SESSION['user_id'])) {
|
||||||
error_log($_SESSION['user_id']);
|
error_log($_SESSION['user_id']);
|
||||||
$userid = $_SESSION['user_id'];
|
$userid = $_SESSION['user_id'];
|
||||||
|
|
||||||
// Prüfen, ob die user_id im Admin-Array ist
|
// Rollen des angemeldeten Benutzers ermitteln
|
||||||
if (in_array($_SESSION['user_id'], $admin_ids, true)) {
|
$roles = fscash_user_roles((int)$_SESSION['user_id'], $admin_ids, $aktien_ids);
|
||||||
|
if ($roles['is_admin']) {
|
||||||
$is_admin = "true";
|
$is_admin = "true";
|
||||||
}
|
}
|
||||||
// Prüfen, ob die user_id im Aktien-Array ist
|
if ($roles['is_aktien']) {
|
||||||
if (in_array($_SESSION['user_id'], $aktien_ids, true)) {
|
|
||||||
$is_aktien = "true";
|
$is_aktien = "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,13 +26,7 @@ $year = isset($_GET['year']) ? (int)$_GET['year'] : (int)date('Y');
|
||||||
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)date('m');
|
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)date('m');
|
||||||
|
|
||||||
// Monats- und Jahresgrenzen prüfen und anpassen
|
// Monats- und Jahresgrenzen prüfen und anpassen
|
||||||
if ($month < 1) {
|
['month' => $month, 'year' => $year] = fscash_normalize_month_year($month, $year);
|
||||||
$month = 12;
|
|
||||||
$year--;
|
|
||||||
} elseif ($month > 12) {
|
|
||||||
$month = 1;
|
|
||||||
$year++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Daten abrufen
|
// Daten abrufen
|
||||||
$sql="SELECT
|
$sql="SELECT
|
||||||
|
|
@ -115,24 +110,18 @@ foreach ($entries as $entry):
|
||||||
|
|
||||||
$kaufpreis = (float)$entry['kaufpreis'];
|
$kaufpreis = (float)$entry['kaufpreis'];
|
||||||
$anzahl = (float)$entry['anzahl'];
|
$anzahl = (float)$entry['anzahl'];
|
||||||
$kaufSumme = $kaufpreis * $anzahl;
|
|
||||||
|
|
||||||
$kurs = isset($entry['kurs']) ? (float)$entry['kurs'] : 0;
|
$kurs = isset($entry['kurs']) ? (float)$entry['kurs'] : 0;
|
||||||
$kursdatum = $entry['kursdatum'];
|
$kursdatum = $entry['kursdatum'];
|
||||||
|
|
||||||
$aktuellerWert = $anzahl * $kurs;
|
$guvData = fscash_aktie_guv($kaufpreis, $anzahl, $kurs);
|
||||||
$guv = $aktuellerWert - $kaufSumme;
|
$kaufSumme = $guvData['kaufSumme'];
|
||||||
|
$aktuellerWert = $guvData['aktuellerWert'];
|
||||||
|
$guv = $guvData['guv'];
|
||||||
|
$guvClass = $guvData['guvClass'];
|
||||||
|
|
||||||
$sumKaufSumme += $kaufSumme;
|
$sumKaufSumme += $kaufSumme;
|
||||||
$sumAktuellerWert += $aktuellerWert;
|
$sumAktuellerWert += $aktuellerWert;
|
||||||
$sumGuV += $guv;
|
$sumGuV += $guv;
|
||||||
|
|
||||||
$guvClass = '';
|
|
||||||
if ($guv > 0) {
|
|
||||||
$guvClass = 'guv-positive';
|
|
||||||
} elseif ($guv < 0) {
|
|
||||||
$guvClass = 'guv-negative';
|
|
||||||
}
|
|
||||||
|
|
||||||
?>
|
?>
|
||||||
<tr>
|
<tr>
|
||||||
<td><?= htmlspecialchars($entry['id']) ?></td>
|
<td><?= htmlspecialchars($entry['id']) ?></td>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require 'config.php';
|
||||||
|
require_once 'lib/fscash.php';
|
||||||
error_log('Session user_id: ' . (isset($_SESSION['user_id']) ? $_SESSION['user_id'] : 'nicht gesetzt'));
|
error_log('Session user_id: ' . (isset($_SESSION['user_id']) ? $_SESSION['user_id'] : 'nicht gesetzt'));
|
||||||
// 1. Session-Prüfung (nur für geschützte Seiten)
|
// 1. Session-Prüfung (nur für geschützte Seiten)
|
||||||
if (!isset($_SESSION['user_id'])) {
|
if (!isset($_SESSION['user_id'])) {
|
||||||
|
|
@ -10,12 +11,12 @@ if (!isset($_SESSION['user_id'])) {
|
||||||
// Ab hier: Nur für angemeldete Benutzer
|
// Ab hier: Nur für angemeldete Benutzer
|
||||||
error_log($_SESSION['user_id']);
|
error_log($_SESSION['user_id']);
|
||||||
|
|
||||||
// Prüfen, ob die user_id im Admin-Array ist
|
// Rollen des angemeldeten Benutzers ermitteln
|
||||||
if (in_array($_SESSION['user_id'], $admin_ids, true)) {
|
$roles = fscash_user_roles((int)$_SESSION['user_id'], $admin_ids, $aktien_ids);
|
||||||
|
if ($roles['is_admin']) {
|
||||||
$is_admin = "true";
|
$is_admin = "true";
|
||||||
}
|
}
|
||||||
// Prüfen, ob die user_id im Aktien-Array ist
|
if ($roles['is_aktien']) {
|
||||||
if (in_array($_SESSION['user_id'], $aktien_ids, true)) {
|
|
||||||
$is_aktien = "true";
|
$is_aktien = "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -24,13 +25,7 @@ $year = isset($_GET['year']) ? (int)$_GET['year'] : (int)date('Y');
|
||||||
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)date('m');
|
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)date('m');
|
||||||
|
|
||||||
// Monats- und Jahresgrenzen prüfen und anpassen
|
// Monats- und Jahresgrenzen prüfen und anpassen
|
||||||
if ($month < 1) {
|
['month' => $month, 'year' => $year] = fscash_normalize_month_year($month, $year);
|
||||||
$month = 12;
|
|
||||||
$year--;
|
|
||||||
} elseif ($month > 12) {
|
|
||||||
$month = 1;
|
|
||||||
$year++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Daten abrufen
|
// Daten abrufen
|
||||||
$sql="WITH user_names AS (
|
$sql="WITH user_names AS (
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
require_once __DIR__ . '/lib/fscash.php';
|
||||||
|
|
||||||
function get2symbol(string $isin, string $openFigiApiKey): ?string
|
function get2symbol(string $isin, string $openFigiApiKey): ?string
|
||||||
{
|
{
|
||||||
$url = "https://api.openfigi.com/v3/mapping";
|
$url = "https://api.openfigi.com/v3/mapping";
|
||||||
|
|
@ -38,26 +40,6 @@ function get2symbol(string $isin, string $openFigiApiKey): ?string
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wir suchen zuerst nach einem Eintrag mit deutschem Handelsplatz (DE)
|
return fscash_select_figi_symbol($data[0]['data']);
|
||||||
$symbolEntry = null;
|
|
||||||
foreach ($data[0]['data'] as $entry) {
|
|
||||||
if (isset($entry['exchCode']) && $entry['exchCode'] === 'XETR') { // XETR = Deutsche Börse Xetra
|
|
||||||
$symbolEntry = $entry;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wenn kein DE/XETR Eintrag gefunden, nehmen wir einfach den ersten
|
|
||||||
if ($symbolEntry === null) {
|
|
||||||
$symbolEntry = $data[0]['data'][0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($symbolEntry['ticker']) && !empty($symbolEntry['ticker'])) {
|
|
||||||
// Kombiniere Ticker und Exchange, um ein vollständiges Symbol zu liefern
|
|
||||||
$exchange = $symbolEntry['exchCode'] ?? '';
|
|
||||||
return $exchange ? "{$symbolEntry['ticker']}.{$exchange}" : $symbolEntry['ticker'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
216
html/lib/fscash.php
Normal file
216
html/lib/fscash.php
Normal file
|
|
@ -0,0 +1,216 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Reine, DB- und netzwerkfreie Hilfsfunktionen für FSCash.
|
||||||
|
*
|
||||||
|
* Diese Datei enthält bewusst nur Funktionen ohne Seiteneffekte (keine
|
||||||
|
* Session-, DB- oder HTTP-Zugriffe), damit sie ohne Testumgebung
|
||||||
|
* (Datenbank, Mock-Server) per Unit-Test geprüft werden können.
|
||||||
|
* Die aufrufenden Seiten unter html/ und migrations/ binden diese Datei ein
|
||||||
|
* und übernehmen weiterhin die Seiteneffekte (DB-Query, HTTP-Request, ...).
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalisiert Monat/Jahr, wenn beim Monats-Blättern (< 1 bzw. > 12) die
|
||||||
|
* Jahresgrenze überschritten wird.
|
||||||
|
*
|
||||||
|
* @return array{month:int,year:int}
|
||||||
|
*/
|
||||||
|
function fscash_normalize_month_year(int $month, int $year): array
|
||||||
|
{
|
||||||
|
if ($month < 1) {
|
||||||
|
$month = 12;
|
||||||
|
$year--;
|
||||||
|
} elseif ($month > 12) {
|
||||||
|
$month = 1;
|
||||||
|
$year++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['month' => $month, 'year' => $year];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt anhand der konfigurierten ID-Listen, ob ein Benutzer Admin- bzw.
|
||||||
|
* Aktien-Rechte hat.
|
||||||
|
*
|
||||||
|
* @param int[] $adminIds
|
||||||
|
* @param int[] $aktienIds
|
||||||
|
* @return array{is_admin:bool,is_aktien:bool}
|
||||||
|
*/
|
||||||
|
function fscash_user_roles(int $userId, array $adminIds, array $aktienIds): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'is_admin' => in_array($userId, $adminIds, true),
|
||||||
|
'is_aktien' => in_array($userId, $aktienIds, true),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Berechnet Kaufsumme, aktuellen Wert, Gewinn/Verlust (GuV) und die
|
||||||
|
* zugehörige CSS-Klasse für einen Aktien-Eintrag.
|
||||||
|
*
|
||||||
|
* @return array{kaufSumme:float,aktuellerWert:float,guv:float,guvClass:string}
|
||||||
|
*/
|
||||||
|
function fscash_aktie_guv(float $kaufpreis, float $anzahl, float $kurs): array
|
||||||
|
{
|
||||||
|
$kaufSumme = $kaufpreis * $anzahl;
|
||||||
|
$aktuellerWert = $anzahl * $kurs;
|
||||||
|
$guv = $aktuellerWert - $kaufSumme;
|
||||||
|
|
||||||
|
$guvClass = '';
|
||||||
|
if ($guv > 0) {
|
||||||
|
$guvClass = 'guv-positive';
|
||||||
|
} elseif ($guv < 0) {
|
||||||
|
$guvClass = 'guv-negative';
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'kaufSumme' => $kaufSumme,
|
||||||
|
'aktuellerWert' => $aktuellerWert,
|
||||||
|
'guv' => $guv,
|
||||||
|
'guvClass' => $guvClass,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wählt aus den von der OpenFIGI-API gelieferten Mapping-Einträgen das
|
||||||
|
* passende Symbol aus. Bevorzugt wird ein Eintrag an der Deutschen Börse
|
||||||
|
* Xetra (XETR), ansonsten wird der erste Eintrag verwendet.
|
||||||
|
*
|
||||||
|
* @param array<int,array{ticker?:string,exchCode?:string}> $figiEntries Der "data"-Teil der OpenFIGI-Antwort.
|
||||||
|
*/
|
||||||
|
function fscash_select_figi_symbol(array $figiEntries): ?string
|
||||||
|
{
|
||||||
|
if (empty($figiEntries)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$symbolEntry = null;
|
||||||
|
foreach ($figiEntries as $entry) {
|
||||||
|
if (isset($entry['exchCode']) && $entry['exchCode'] === 'XETR') {
|
||||||
|
$symbolEntry = $entry;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($symbolEntry === null) {
|
||||||
|
$symbolEntry = $figiEntries[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($symbolEntry['ticker']) && !empty($symbolEntry['ticker'])) {
|
||||||
|
$exchange = $symbolEntry['exchCode'] ?? '';
|
||||||
|
return $exchange ? "{$symbolEntry['ticker']}.{$exchange}" : $symbolEntry['ticker'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt die im Personenreport ausgewählten Kategorie-IDs: die per
|
||||||
|
* Request übergebenen, sofern vorhanden, sonst alle verfügbaren.
|
||||||
|
*
|
||||||
|
* @param int[] $allCategoryIds Alle in der DB vorhandenen Kategorie-IDs.
|
||||||
|
* @param mixed $requestedCategoryIds Roher Wert aus $_GET['categories'] (oder null, falls nicht gesetzt).
|
||||||
|
* @return array<int,mixed>
|
||||||
|
*/
|
||||||
|
function fscash_resolve_selected_categories(array $allCategoryIds, $requestedCategoryIds): array
|
||||||
|
{
|
||||||
|
if (is_array($requestedCategoryIds) && $requestedCategoryIds !== []) {
|
||||||
|
return $requestedCategoryIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $allCategoryIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extrahiert den aktuellen Kurs aus einer Alpha-Vantage GLOBAL_QUOTE-Antwort.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $alphaVantageResponse
|
||||||
|
*/
|
||||||
|
function fscash_extract_quote_price(array $alphaVantageResponse): ?float
|
||||||
|
{
|
||||||
|
$price = $alphaVantageResponse['Global Quote']['05. price'] ?? null;
|
||||||
|
|
||||||
|
if ($price === null || $price === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (float)$price;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wandelt die Alpha-Vantage SYMBOL_SEARCH-Antwort in die vom Frontend
|
||||||
|
* erwartete, vereinfachte Liste (symbol/name/region) um.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $alphaVantageResponse
|
||||||
|
* @return array<int,array{symbol:string,name:string,region:string}>
|
||||||
|
*/
|
||||||
|
function fscash_map_symbol_search_results(array $alphaVantageResponse): array
|
||||||
|
{
|
||||||
|
$matches = $alphaVantageResponse['bestMatches'] ?? [];
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($matches as $match) {
|
||||||
|
$result[] = [
|
||||||
|
'symbol' => $match['1. symbol'] ?? '',
|
||||||
|
'name' => $match['2. name'] ?? '',
|
||||||
|
'region' => $match['4. region'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prüft, ob die per JSON-Body übermittelten Daten zum Speichern eines
|
||||||
|
* Aktien-Symbols gültig sind (id und symbol müssen "truthy" sein).
|
||||||
|
*
|
||||||
|
* @param mixed $id
|
||||||
|
* @param mixed $symbol
|
||||||
|
*/
|
||||||
|
function fscash_is_valid_symbol_payload($id, $symbol): bool
|
||||||
|
{
|
||||||
|
return (bool)$id && (bool)$symbol;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut aus einem DB-Eintrag des Monatsreports die Zeile für den CSV-Export.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed> $entry
|
||||||
|
* @return array<int,string>
|
||||||
|
*/
|
||||||
|
function fscash_format_report_month_csv_row(array $entry): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
(string)($entry['id'] ?? ''),
|
||||||
|
(string)($entry['datum'] ?? ''),
|
||||||
|
(string)($entry['benutzer_name'] ?? ''),
|
||||||
|
(string)($entry['kategorie_id'] ?? ''),
|
||||||
|
(string)($entry['kategorie_name'] ?? ''),
|
||||||
|
(string)($entry['betreff'] ?? ''),
|
||||||
|
number_format((float)($entry['betrag'] ?? 0), 2, ',', '.'),
|
||||||
|
(string)($entry['zahlweise'] ?? $entry['zahlungsmethode'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ermittelt aus allen verfügbaren Migrations-Dateien diejenigen, die laut
|
||||||
|
* schema_migrations-Tabelle noch nicht angewendet wurden. Die Rückgabe ist
|
||||||
|
* alphabetisch/numerisch sortiert, damit Migrationen in der richtigen
|
||||||
|
* Reihenfolge angewendet werden.
|
||||||
|
*
|
||||||
|
* @param string[] $availableFiles Absolute oder relative Pfade der *.sql-Dateien.
|
||||||
|
* @param string[] $appliedFilenames Basenamen der bereits angewendeten Migrationen.
|
||||||
|
* @return string[] Pfade der noch ausstehenden Migrationen, sortiert.
|
||||||
|
*/
|
||||||
|
function fscash_pending_migrations(array $availableFiles, array $appliedFilenames): array
|
||||||
|
{
|
||||||
|
$files = $availableFiles;
|
||||||
|
sort($files, SORT_STRING);
|
||||||
|
|
||||||
|
return array_values(array_filter(
|
||||||
|
$files,
|
||||||
|
fn(string $f): bool => !in_array(basename($f), $appliedFilenames, true)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require 'config.php';
|
||||||
|
require_once 'lib/fscash.php';
|
||||||
if (!isset($_SESSION['user_id'])) {
|
if (!isset($_SESSION['user_id'])) {
|
||||||
header('Location: index.php');
|
header('Location: index.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfen, ob die user_id im Admin-Array ist
|
// Rollen des angemeldeten Benutzers ermitteln
|
||||||
if (in_array($_SESSION['user_id'], $admin_ids, true)) {
|
$roles = fscash_user_roles((int)$_SESSION['user_id'], $admin_ids, $aktien_ids);
|
||||||
|
if ($roles['is_admin']) {
|
||||||
$is_admin = "true";
|
$is_admin = "true";
|
||||||
}
|
}
|
||||||
// Prüfen, ob die user_id im Aktien-Array ist
|
if ($roles['is_aktien']) {
|
||||||
if (in_array($_SESSION['user_id'], $aktien_ids, true)) {
|
|
||||||
$is_aktien = "true";
|
$is_aktien = "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,13 +20,7 @@ $year = isset($_GET['year']) ? (int)$_GET['year'] : (int)date('Y');
|
||||||
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)date('m');
|
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)date('m');
|
||||||
|
|
||||||
// Monats- und Jahresgrenzen prüfen und anpassen
|
// Monats- und Jahresgrenzen prüfen und anpassen
|
||||||
if ($month < 1) {
|
['month' => $month, 'year' => $year] = fscash_normalize_month_year($month, $year);
|
||||||
$month = 12;
|
|
||||||
$year--;
|
|
||||||
} elseif ($month > 12) {
|
|
||||||
$month = 1;
|
|
||||||
$year++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Daten für die Summen-Tabelle abrufen
|
// Daten für die Summen-Tabelle abrufen
|
||||||
$sql_sum = "
|
$sql_sum = "
|
||||||
|
|
@ -94,16 +89,7 @@ if (isset($_GET['export']) && $_GET['export'] === 'csv') {
|
||||||
$output = fopen('php://output', 'w');
|
$output = fopen('php://output', 'w');
|
||||||
fputcsv($output, ['ID', 'Datum', 'Benutzer', 'Kat-ID', 'Kategorie', 'Betreff', 'Betrag', 'Zahlungsmethode'], ';');
|
fputcsv($output, ['ID', 'Datum', 'Benutzer', 'Kat-ID', 'Kategorie', 'Betreff', 'Betrag', 'Zahlungsmethode'], ';');
|
||||||
foreach ($entries_csv as $entry) {
|
foreach ($entries_csv as $entry) {
|
||||||
fputcsv($output, [
|
fputcsv($output, fscash_format_report_month_csv_row($entry), ';');
|
||||||
$entry['id'] ?? '',
|
|
||||||
$entry['datum'] ?? '',
|
|
||||||
$entry['benutzer_name'] ?? '',
|
|
||||||
$entry['kategorie_id'] ?? '',
|
|
||||||
$entry['kategorie_name'] ?? '',
|
|
||||||
$entry['betreff'] ?? '',
|
|
||||||
number_format($entry['betrag'], 2, ',', '.'),
|
|
||||||
$entry['zahlungsmethode'] ?? ''
|
|
||||||
], ';');
|
|
||||||
}
|
}
|
||||||
fclose($output);
|
fclose($output);
|
||||||
exit;
|
exit;
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require 'config.php';
|
||||||
|
require_once 'lib/fscash.php';
|
||||||
if (!isset($_SESSION['user_id'])) {
|
if (!isset($_SESSION['user_id'])) {
|
||||||
header('Location: index.php');
|
header('Location: index.php');
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prüfen, ob die user_id im Admin-Array ist
|
// Rollen des angemeldeten Benutzers ermitteln
|
||||||
if (in_array($_SESSION['user_id'], $admin_ids, true)) {
|
$roles = fscash_user_roles((int)$_SESSION['user_id'], $admin_ids, $aktien_ids);
|
||||||
|
if ($roles['is_admin']) {
|
||||||
$is_admin = "true";
|
$is_admin = "true";
|
||||||
}
|
}
|
||||||
// Prüfen, ob die user_id im Aktien-Array ist
|
if ($roles['is_aktien']) {
|
||||||
if (in_array($_SESSION['user_id'], $aktien_ids, true)) {
|
|
||||||
$is_aktien = "true";
|
$is_aktien = "true";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,24 +20,17 @@ $year = isset($_GET['year']) ? (int)$_GET['year'] : (int)date('Y');
|
||||||
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)date('m');
|
$month = isset($_GET['month']) ? (int)$_GET['month'] : (int)date('m');
|
||||||
|
|
||||||
// Monats- und Jahresgrenzen prüfen und anpassen
|
// Monats- und Jahresgrenzen prüfen und anpassen
|
||||||
if ($month < 1) {
|
['month' => $month, 'year' => $year] = fscash_normalize_month_year($month, $year);
|
||||||
$month = 12;
|
|
||||||
$year--;
|
|
||||||
} elseif ($month > 12) {
|
|
||||||
$month = 1;
|
|
||||||
$year++;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kategorien aus der Datenbank abrufen
|
// Kategorien aus der Datenbank abrufen
|
||||||
$categoryStmt = $pdo->query("SELECT category_id, name FROM categories ORDER BY name");
|
$categoryStmt = $pdo->query("SELECT category_id, name FROM categories ORDER BY name");
|
||||||
$categories = $categoryStmt->fetchAll();
|
$categories = $categoryStmt->fetchAll();
|
||||||
|
|
||||||
// Standardmäßig alle Kategorien auswählen, falls keine Auswahl getroffen wurde
|
// Standardmäßig alle Kategorien auswählen, falls keine Auswahl getroffen wurde
|
||||||
if (array_key_exists('categories', $_GET) && $_GET['categories'] !== []) {
|
$selectedCategories = fscash_resolve_selected_categories(
|
||||||
$selectedCategories = $_GET['categories'];
|
array_column($categories, 'category_id'),
|
||||||
} else {
|
$_GET['categories'] ?? null
|
||||||
$selectedCategories = array_column($categories, 'category_id');
|
);
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Daten abrufen für den ausgewählten Monat und die ausgewählten Kategorien
|
// Daten abrufen für den ausgewählten Monat und die ausgewählten Kategorien
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require 'config.php';
|
||||||
|
require_once 'lib/fscash.php';
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
// JSON-Input einlesen
|
// JSON-Input einlesen
|
||||||
|
|
@ -8,7 +9,7 @@ $id = $data['id'] ?? null;
|
||||||
$symbol = $data['symbol'] ?? null;
|
$symbol = $data['symbol'] ?? null;
|
||||||
|
|
||||||
// Input validieren
|
// Input validieren
|
||||||
if (!$id || !$symbol) {
|
if (!fscash_is_valid_symbol_payload($id, $symbol)) {
|
||||||
echo json_encode([
|
echo json_encode([
|
||||||
'success' => false,
|
'success' => false,
|
||||||
'message' => 'Ungültige Eingabedaten.'
|
'message' => 'Ungültige Eingabedaten.'
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require 'config.php';
|
||||||
|
require_once 'lib/fscash.php';
|
||||||
|
|
||||||
header('Content-Type: application/json');
|
header('Content-Type: application/json');
|
||||||
|
|
||||||
|
|
@ -18,15 +19,6 @@ if (!$response) {
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = json_decode($response, true);
|
$data = json_decode($response, true);
|
||||||
$matches = $data['bestMatches'] ?? [];
|
$result = fscash_map_symbol_search_results($data ?? []);
|
||||||
|
|
||||||
$result = [];
|
|
||||||
foreach ($matches as $match) {
|
|
||||||
$result[] = [
|
|
||||||
'symbol' => $match['1. symbol'],
|
|
||||||
'name' => $match['2. name'],
|
|
||||||
'region' => $match['4. region']
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
echo json_encode($result);
|
echo json_encode($result);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
require 'config.php';
|
require 'config.php';
|
||||||
require 'get_symbol.php';
|
require 'get_symbol.php';
|
||||||
|
require_once 'lib/fscash.php';
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -60,10 +61,10 @@ try {
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
$data = json_decode($json, true);
|
$data = json_decode($json, true);
|
||||||
|
$kurs = fscash_extract_quote_price($data ?? []);
|
||||||
|
|
||||||
if (isset($data['Global Quote']['05. price'])) {
|
if ($kurs !== null) {
|
||||||
|
|
||||||
$kurs = (float)$data['Global Quote']['05. price'];
|
|
||||||
$kursdatum = date('Y-m-d');
|
$kursdatum = date('Y-m-d');
|
||||||
|
|
||||||
// 🔹 Kurs UND Symbol speichern
|
// 🔹 Kurs UND Symbol speichern
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<?php
|
<?php
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../html/lib/fscash.php';
|
||||||
|
|
||||||
$configPath = $argv[1] ?? '/var/www/html/config.php';
|
$configPath = $argv[1] ?? '/var/www/html/config.php';
|
||||||
$migrationsDir = __DIR__;
|
$migrationsDir = __DIR__;
|
||||||
|
|
||||||
|
|
@ -26,12 +28,8 @@ $pdo->exec(
|
||||||
$applied = $pdo->query("SELECT filename FROM schema_migrations")->fetchAll(PDO::FETCH_COLUMN);
|
$applied = $pdo->query("SELECT filename FROM schema_migrations")->fetchAll(PDO::FETCH_COLUMN);
|
||||||
|
|
||||||
$files = glob($migrationsDir . '/*.sql') ?: [];
|
$files = glob($migrationsDir . '/*.sql') ?: [];
|
||||||
sort($files, SORT_STRING);
|
|
||||||
|
|
||||||
$pending = array_values(array_filter(
|
$pending = fscash_pending_migrations($files, $applied);
|
||||||
$files,
|
|
||||||
fn(string $f): bool => !in_array(basename($f), $applied, true)
|
|
||||||
));
|
|
||||||
|
|
||||||
if (empty($pending)) {
|
if (empty($pending)) {
|
||||||
echo "Keine neuen Migrationen.\n";
|
echo "Keine neuen Migrationen.\n";
|
||||||
|
|
|
||||||
13
phpunit.xml
Normal file
13
phpunit.xml
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
|
||||||
|
bootstrap="vendor/autoload.php"
|
||||||
|
colors="true"
|
||||||
|
failOnWarning="true"
|
||||||
|
failOnDeprecation="false">
|
||||||
|
<testsuites>
|
||||||
|
<testsuite name="FSCash">
|
||||||
|
<directory>tests</directory>
|
||||||
|
</testsuite>
|
||||||
|
</testsuites>
|
||||||
|
</phpunit>
|
||||||
307
tests/FscashTest.php
Normal file
307
tests/FscashTest.php
Normal file
|
|
@ -0,0 +1,307 @@
|
||||||
|
<?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_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));
|
||||||
|
}
|
||||||
|
}
|
||||||
19
tests/run.sh
Executable file
19
tests/run.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# Führt die PHPUnit-Testsuite aus. Wird von update.sh vor jedem Deploy
|
||||||
|
# automatisch aufgerufen (siehe README/migrations für die Gesamt-Pipeline).
|
||||||
|
#
|
||||||
|
# Voraussetzung: `composer install` wurde im Projektverzeichnis ausgeführt
|
||||||
|
# (vendor/bin/phpunit muss existieren).
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
cd "$PROJECT_DIR"
|
||||||
|
|
||||||
|
if [ ! -x vendor/bin/phpunit ]; then
|
||||||
|
echo "FEHLER: vendor/bin/phpunit fehlt - bitte zuerst 'composer install' ausführen." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
vendor/bin/phpunit --colors=never
|
||||||
25
update.sh
25
update.sh
|
|
@ -40,7 +40,20 @@ while IFS= read -r -d '' file; do
|
||||||
php -l "$file" >/dev/null || fail "Syntaxfehler in $file - es wird nichts kopiert."
|
php -l "$file" >/dev/null || fail "Syntaxfehler in $file - es wird nichts kopiert."
|
||||||
done < <(find html -name '*.php' -print0)
|
done < <(find html -name '*.php' -print0)
|
||||||
|
|
||||||
# --- 4. Dateien kopieren, config.php bleibt unangetastet ---
|
# --- 4. Unit-Tests: müssen grün sein, bevor irgendetwas deployt wird ---
|
||||||
|
if [ -f tests/run.sh ]; then
|
||||||
|
if [ ! -x vendor/bin/phpunit ]; then
|
||||||
|
command -v composer >/dev/null 2>&1 || fail "composer ist nicht installiert - wird für die Tests benötigt."
|
||||||
|
log "Installiere Test-Abhängigkeiten (composer install)..."
|
||||||
|
composer install --no-interaction --no-progress
|
||||||
|
fi
|
||||||
|
log "Führe Tests aus..."
|
||||||
|
bash tests/run.sh || fail "Tests fehlgeschlagen - es wird nichts deployt. Siehe Ausgabe oben."
|
||||||
|
else
|
||||||
|
log "Noch keine Tests vorhanden - werden später ergänzt."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 5. Dateien kopieren, config.php bleibt unangetastet ---
|
||||||
log "Kopiere Dateien nach $WEBROOT..."
|
log "Kopiere Dateien nach $WEBROOT..."
|
||||||
config_backup=""
|
config_backup=""
|
||||||
if [ -f "$CONFIG_FILE" ]; then
|
if [ -f "$CONFIG_FILE" ]; then
|
||||||
|
|
@ -55,18 +68,10 @@ if [ -n "$config_backup" ]; then
|
||||||
rm -f "$config_backup"
|
rm -f "$config_backup"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- 5. Datenbank-Migrationen ---
|
# --- 6. Datenbank-Migrationen ---
|
||||||
if [ -d migrations ] && [ -f migrations/run_migrations.php ]; then
|
if [ -d migrations ] && [ -f migrations/run_migrations.php ]; then
|
||||||
log "Wende Datenbank-Migrationen an..."
|
log "Wende Datenbank-Migrationen an..."
|
||||||
php migrations/run_migrations.php "$CONFIG_FILE"
|
php migrations/run_migrations.php "$CONFIG_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# --- 6. Tests ---
|
|
||||||
if [ -f tests/run.sh ]; then
|
|
||||||
log "Führe Tests aus..."
|
|
||||||
bash tests/run.sh
|
|
||||||
else
|
|
||||||
log "Noch keine Tests vorhanden - werden später ergänzt."
|
|
||||||
fi
|
|
||||||
|
|
||||||
log "Update abgeschlossen."
|
log "Update abgeschlossen."
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue