adressen();
$this->meldungen();
$this->yahooAusMarkup();
$this->yahooOhneKuerzel();
}
/**
* Yahoo liefert die Kursseite in zwei Ausprägungen aus – beide müssen
* denselben Kurs ergeben. Das Markup stammt aus den echten Seiten vom
* 13.08.2026, gekürzt auf das Wesentliche.
*/
private function yahooAusMarkup(): void
{
$streamer = '
Allianz SE (ALV.DE) Aktienkurs – Yahoo Finanzen'
. '';
$this->assertEquals(441.7, Kursabruf::yahooKurs($streamer, 'ALV.DE'), 'Kurs aus ');
$angezeigt = 'SAP SE (SAP.DE) Aktienkurs – Yahoo Finanzen'
. '177,22 '
. '+1,26 ';
$this->assertEquals(177.22, Kursabruf::yahooKurs($angezeigt, 'SAP.DE'), 'Kurs aus dem angezeigten Preis');
$tausender = 'Test (XYZ.DE) – Yahoo Finanzen'
. '1.234,56 ';
$this->assertEquals(1234.56, Kursabruf::yahooKurs($tausender, 'XYZ.DE'), 'Tausenderpunkt und Dezimalkomma');
// Die Indexleiste am Seitenkopf führt fremde Kurse mit – die dürfen
// nicht als Kurs des Papiers durchgehen.
$fremd = 'Allianz SE (ALV.DE) Aktienkurs – Yahoo Finanzen'
. ''
. '';
$this->assertEquals(441.7, Kursabruf::yahooKurs($fremd, 'ALV.DE'), 'Der DAX-Stand der Kopfleiste zählt nicht');
$this->assertNull(
Kursabruf::yahooKurs('Allianz SE (ALV.DE)Umbau
', 'ALV.DE'),
'Ohne bekanntes Muster kommt kein Kurs statt eines geratenen'
);
// Yahoo zeigt für Papiere ohne Umsatz „--“ an – das ist kein Kurs.
$this->assertNull(
Kursabruf::yahooKurs(
'Commerce Bancshares Inc (CB5.DE)--',
'CB5.DE'
),
'Ein Strich statt einer Zahl gilt nicht als Kurs'
);
// Ein falsches Kürzel darf keinen fremden Kurs in den Bestand tragen.
$fehler = '';
try {
Kursabruf::yahooKurs('Siemens AG (SIE.DE) Aktienkurs'
. '210,00', 'ALV.DE');
} catch (\RuntimeException $e) {
$fehler = $e->getMessage();
}
$this->assert(str_contains($fehler, 'ALV.DE'), 'Eine fremde Seite wird am Titel erkannt');
}
/** Ohne Kürzel kann Yahoo nichts finden – das muss die Meldung sagen. */
private function yahooOhneKuerzel(): void
{
$kurs = new ReflectionMethod(Kursabruf::class, 'yahoo');
$kurs->setAccessible(true);
$fehler = '';
try {
$kurs->invoke(null, ['id' => 0, 'symbol' => ' ', 'isin' => 'DE0008404005', 'name' => 'Allianz']);
} catch (\RuntimeException $e) {
$fehler = $e->getMessage();
}
$this->assert(str_contains($fehler, 'Kürzel'), 'Das fehlende Kürzel wird benannt');
$this->assert(str_contains($fehler, 'ALV.DE'), 'Die Meldung nennt ein Beispiel');
}
/** Keine abgeschalteten Adressen mehr im Quelltext. */
private function adressen(): void
{
$quelle = (string)file_get_contents(dirname(__DIR__) . '/src/Services/Kursabruf.php');
$this->assert(
!str_contains($quelle, 'financialmodelingprep.com/api/v3'),
'Financial Modeling Prep wird nicht mehr über /api/v3 angesprochen'
);
$this->assert(
str_contains($quelle, 'financialmodelingprep.com/stable/quote?symbol='),
'Der Kurs kommt über /stable/quote'
);
$this->assert(
str_contains($quelle, 'financialmodelingprep.com/stable/search-isin?isin='),
'Die ISIN wird über /stable/search-isin aufgelöst'
);
}
/**
* Die Begründung des Dienstes gehört in die Fehlermeldung – sonst sucht man
* beim Schlüssel, wenn die Adresse das Problem ist.
*/
private function meldungen(): void
{
$meldung = new ReflectionMethod(Kursabruf::class, 'meldung');
$meldung->setAccessible(true);
$json = (string)$meldung->invoke(null, '{"Error Message": "Legacy Endpoint : nicht mehr\nunterstützt"}');
$this->assertEquals(
' Legacy Endpoint : nicht mehr unterstützt',
$json,
'Aus JSON wird der Klartext einzeilig übernommen'
);
$text = (string)$meldung->invoke(null, 'Restricted Endpoint: nicht im Tarif');
$this->assertEquals(
' Restricted Endpoint: nicht im Tarif',
$text,
'Antworten ohne JSON werden unverändert übernommen'
);
$this->assertEquals('', (string)$meldung->invoke(null, ' '), 'Leere Antworten hängen nichts an');
$this->assertEquals('', (string)$meldung->invoke(null, '[]'), 'Ein leeres Feld hängt nichts an');
$lang = (string)$meldung->invoke(null, str_repeat('x', 500));
// Ein Leerzeichen davor, 200 Zeichen Text, ein Auslassungszeichen dahinter.
$this->assertEquals(202, mb_strlen($lang), 'Lange Antworten werden auf 200 Zeichen gekürzt');
$this->assert(str_ends_with($lang, '…'), 'Gekürzte Antworten enden mit Auslassungszeichen');
}
}