- Navigations-Chrome (CategoryNavigation), Suche- und Newsroom-Seitendaten 300s gecacht; gemeinsamer WebPageCache-Helper (eine TTL/Key-Konvention). - Cache-Poisoning über ?page= geschlossen (Key an Paginator ausgerichtet, kein withQueryString im Cache); Newsrooms cachet nur den ungefilterten Fall und die ersten Seiten (kein unbegrenzter Key-Raum). - Detailseiten schreiben Hits/View-Events deferred nach der Response (M12); OpenAI-/Markt-/USt-ID-Aufrufe mit Connect-Timeouts und begrenzten Retries; Admin-Memo-Caches konsistent invalidiert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
99 lines
3.1 KiB
PHP
99 lines
3.1 KiB
PHP
<?php
|
|
|
|
use App\Services\Web\MarketDataService;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
|
|
beforeEach(function () {
|
|
Cache::forget((string) config('market.cache_key'));
|
|
|
|
config()->set('market.enabled', true);
|
|
config()->set('market.symbols', [
|
|
['label' => 'DAX', 'symbol' => '^GDAXI', 'precision' => 0, 'suffix' => ''],
|
|
['label' => 'EUR/USD', 'symbol' => 'EURUSD=X', 'precision' => 2, 'suffix' => ''],
|
|
]);
|
|
});
|
|
|
|
function fakeYahooChart(float $price, float $previousClose): array
|
|
{
|
|
return [
|
|
'chart' => [
|
|
'result' => [
|
|
['meta' => ['regularMarketPrice' => $price, 'chartPreviousClose' => $previousClose]],
|
|
],
|
|
],
|
|
];
|
|
}
|
|
|
|
it('holt Kurse, berechnet Deltas und cacht die Slides', function () {
|
|
Http::fake([
|
|
'*GDAXI*' => Http::response(fakeYahooChart(24150.0, 24000.0)),
|
|
'*EURUSD*' => Http::response(fakeYahooChart(1.0815, 1.0902)),
|
|
]);
|
|
|
|
$slides = app(MarketDataService::class)->refresh();
|
|
|
|
expect($slides)->toHaveCount(2)
|
|
->and($slides[0]['label'])->toBe('DAX')
|
|
->and($slides[0]['value'])->toBe('24.150')
|
|
->and($slides[0]['delta'])->toBe('+0,6%')
|
|
->and($slides[0]['positive'])->toBeTrue()
|
|
->and($slides[1]['delta'])->toBe('-0,8%')
|
|
->and($slides[1]['positive'])->toBeFalse();
|
|
|
|
Http::fake();
|
|
|
|
expect(app(MarketDataService::class)->slides())->toBe($slides);
|
|
});
|
|
|
|
it('behält den Cache-Stand, wenn der Abruf komplett fehlschlägt', function () {
|
|
Cache::put((string) config('market.cache_key'), [
|
|
['label' => 'DAX', 'value' => '24.150', 'delta' => '+0,6%', 'positive' => true],
|
|
], 3600);
|
|
|
|
Http::fake(['*' => Http::response(null, 500)]);
|
|
|
|
$slides = app(MarketDataService::class)->refresh();
|
|
|
|
expect($slides)->toHaveCount(1)
|
|
->and($slides[0]['value'])->toBe('24.150');
|
|
Http::assertSentCount(6);
|
|
});
|
|
|
|
it('liefert keine Slides, wenn Marktdaten deaktiviert sind', function () {
|
|
config()->set('market.enabled', false);
|
|
|
|
Cache::put((string) config('market.cache_key'), [
|
|
['label' => 'DAX', 'value' => '24.150', 'delta' => '+0,6%', 'positive' => true],
|
|
], 3600);
|
|
|
|
expect(app(MarketDataService::class)->slides())->toBe([]);
|
|
});
|
|
|
|
it('überspringt Symbole mit unbrauchbarer API-Antwort', function () {
|
|
Http::fake([
|
|
'*GDAXI*' => Http::response(['chart' => ['result' => [['meta' => []]]]]),
|
|
'*EURUSD*' => Http::response(fakeYahooChart(1.0815, 1.0902)),
|
|
]);
|
|
|
|
$slides = app(MarketDataService::class)->refresh();
|
|
|
|
expect($slides)->toHaveCount(1)
|
|
->and($slides[0]['label'])->toBe('EUR/USD');
|
|
});
|
|
|
|
it('aktualisiert Kurse über den Artisan-Command', function () {
|
|
Http::fake(['*' => Http::response(fakeYahooChart(24150.0, 24000.0))]);
|
|
|
|
$this->artisan('market:refresh')
|
|
->expectsOutputToContain('DAX')
|
|
->assertSuccessful();
|
|
|
|
expect(app(MarketDataService::class)->slides())->not->toBeEmpty();
|
|
});
|
|
|
|
it('meldet einen Fehlschlag des Commands ohne Kursdaten', function () {
|
|
Http::fake(['*' => Http::response(null, 500)]);
|
|
|
|
$this->artisan('market:refresh')->assertFailed();
|
|
});
|