presseportale/tests/Feature/Web/PageDataCacheTest.php
Kevin Adametz 23cbd0bfe2 performance: Caching, deferred Tracking & externe Timeouts (H7, M12, M13)
- 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>
2026-07-14 13:49:47 +00:00

271 lines
9.6 KiB
PHP

<?php
use App\Enums\Portal;
use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use Illuminate\Support\Facades\Cache;
use Livewire\Volt\Volt as LivewireVolt;
use Tests\TestCase;
function makeCategoryWithSlug(Portal $portal, string $name, string $slug, ?int $parentId = null): Category
{
$category = Category::factory()->create(['portal' => $portal, 'parent_id' => $parentId]);
$category->translations()->create([
'locale' => 'de',
'name' => $name,
'slug' => $slug,
]);
return $category;
}
test('homepage data is cached per portal and language', function () {
/** @var TestCase $this */
$category = makeCategoryWithSlug(Portal::Presseecho, 'Energie & Klima', 'energie-klima');
$company = Company::factory()->presseecho()->create();
PressRelease::factory()
->published()
->forPortal(Portal::Presseecho)
->for($category)
->for($company)
->create([
'title' => 'Erste Meldung im Cache',
'slug' => 'erste-meldung-im-cache',
'published_at' => now()->subHour(),
]);
$this->get('https://presseecho.test/de')
->assertSuccessful()
->assertSeeText('Erste Meldung im Cache');
expect(Cache::has('web.home.presseecho.de'))->toBeTrue();
PressRelease::factory()
->published()
->forPortal(Portal::Presseecho)
->for($category)
->for($company)
->create([
'title' => 'Neue Meldung nach Cache-Aufbau',
'slug' => 'neue-meldung-nach-cache-aufbau',
'published_at' => now()->subMinutes(5),
]);
// Innerhalb der TTL liefert HomePageData den gecachten Stand: Lead bleibt
// die alte Meldung. (Der Livewire-Feed ist bewusst ungecacht und zeigt
// neue Meldungen sofort.)
$cachedLead = $this->get('https://presseecho.test/de')
->assertSuccessful()
->viewData('leadRelease');
expect($cachedLead->title)->toBe('Erste Meldung im Cache');
Cache::flush();
$freshLead = $this->get('https://presseecho.test/de')
->assertSuccessful()
->viewData('leadRelease');
expect($freshLead->title)->toBe('Neue Meldung nach Cache-Aufbau');
});
test('category page data is cached per portal, language and slug but 404s are not cached', function () {
/** @var TestCase $this */
$category = makeCategoryWithSlug(Portal::Businessportal24, 'Mobilität', 'mobilitaet');
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($category)
->for(Company::factory()->businessportal24())
->create([
'slug' => 'mobilitaets-meldung',
'published_at' => now()->subDay(),
]);
$this->get('https://businessportal24.test/de/category/mobilitaet')->assertSuccessful();
expect(Cache::has('web.category.businessportal24.de.mobilitaet'))->toBeTrue();
$this->get('https://businessportal24.test/de/category/gibt-es-nicht')->assertNotFound();
expect(Cache::has('web.category.businessportal24.de.gibt-es-nicht'))->toBeFalse();
});
test('search sidebar data is cached per portal and language', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de/suche')->assertSuccessful();
expect(Cache::has('web.search.most-read.businessportal24-both.de'))->toBeTrue();
});
test('newsroom detail data is cached per portal language slug and page', function () {
/** @var TestCase $this */
$company = Company::factory()->businessportal24()->create([
'slug' => 'cache-newsroom',
'is_active' => true,
]);
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->create([
'company_id' => $company->id,
'language' => 'de',
'published_at' => now()->subDay(),
]);
$this->get('https://businessportal24.test/de/newsroom/cache-newsroom?page=2')
->assertSuccessful();
expect(Cache::has('web.newsroom.businessportal24-both.de.cache-newsroom.page.2'))->toBeTrue();
});
test('newsroom detail cache key aligns with the paginator so an invalid page cannot poison another page', function () {
/** @var TestCase $this */
$company = Company::factory()->businessportal24()->create([
'slug' => 'poison-newsroom',
'is_active' => true,
]);
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->create([
'company_id' => $company->id,
'language' => 'de',
'published_at' => now()->subDay(),
]);
// Ungültige Seitennummer: der Paginator rendert Seite 1, also darf der
// Inhalt NICHT unter dem Key für Seite 2 landen (sonst Cache-Poisoning).
$this->get('https://businessportal24.test/de/newsroom/poison-newsroom?page=2.5')
->assertSuccessful();
expect(Cache::has('web.newsroom.businessportal24-both.de.poison-newsroom.page.1'))->toBeTrue();
expect(Cache::has('web.newsroom.businessportal24-both.de.poison-newsroom.page.2'))->toBeFalse();
});
test('newsrooms unfiltered index is cached but filtered searches are not', function () {
/** @var TestCase $this */
$company = Company::factory()->businessportal24()->create([
'name' => 'SolarTec Bestand',
'is_active' => true,
]);
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($company)
->create(['language' => 'de', 'published_at' => now()->subDay()]);
// Ungefiltertes Verzeichnis (erste Seite) wird unter einem beschränkten Key gecacht.
$this->get('https://businessportal24.test/de/newsrooms')
->assertSuccessful()
->assertSeeText('SolarTec Bestand');
expect(Cache::has('web.newsrooms.businessportal24,both.de.page.1'))->toBeTrue();
// Gefilterte Suche wird NICHT gecacht (Schutz vor unbegrenztem Key-Raum):
// eine nach dem ersten Aufruf angelegte Firma erscheint sofort.
$newCompany = Company::factory()->businessportal24()->create([
'name' => 'SolarTec Neu',
'is_active' => true,
]);
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($newCompany)
->create(['language' => 'de', 'published_at' => now()->subHour()]);
$this->get('https://businessportal24.test/de/newsrooms?q=solartec')
->assertSuccessful()
->assertSeeText('SolarTec Neu');
$this->get('https://businessportal24.test/de/newsrooms?q=solartec')
->assertSuccessful()
->assertSeeText('SolarTec Neu');
});
test('deep newsrooms pages are not cached', function () {
/** @var TestCase $this */
$this->get('https://businessportal24.test/de/newsrooms?page=25')->assertSuccessful();
expect(Cache::has('web.newsrooms.businessportal24,both.de.page.25'))->toBeFalse();
});
test('newsrooms cache does not retain unrelated query parameters in pagination links', function () {
/** @var TestCase $this */
$response = $this->get('https://businessportal24.test/de/newsrooms?q=SolarTec&utm_source=poison')
->assertSuccessful();
$pageTwoUrl = $response->viewData('newsrooms')->url(2);
expect($pageTwoUrl)
->toContain('q=SolarTec')
->not->toContain('utm_source');
});
test('subcategory counts are aggregated correctly in a single grouped query', function () {
/** @var TestCase $this */
$parent = makeCategoryWithSlug(Portal::Both, 'Wirtschaft', 'wirtschaft');
$child = makeCategoryWithSlug(Portal::Both, 'Handel', 'handel', $parent->id);
$company = Company::factory()->businessportal24()->create();
// Stichtag (referenceNow) = jüngste Meldung: 2 im 7-Tage-Fenster, 1 älter (>14 Tage).
foreach ([1, 2, 20] as $daysAgo) {
PressRelease::factory()
->published()
->forPortal(Portal::Businessportal24)
->for($child)
->for($company)
->create([
'slug' => "handel-meldung-{$daysAgo}",
'published_at' => now()->subDays($daysAgo),
]);
}
$response = $this->get('https://businessportal24.test/de/category/wirtschaft')->assertSuccessful();
$subCategories = $response->viewData('subCategories');
expect($subCategories)->toHaveCount(1)
->and($subCategories[0]['name'])->toBe('Handel')
->and($subCategories[0]['total'])->toBe(3)
->and($subCategories[0]['recent'])->toBe(2)
->and($subCategories[0]['delta'])->toBe(2);
});
test('the press release feed today filter matches the reference day via range comparison', function () {
/** @var TestCase $this */
$category = makeCategoryWithSlug(Portal::Presseecho, 'Technologie', 'technologie');
$company = Company::factory()->presseecho()->create();
// Historisches Archiv: Stichtag = jüngste Meldung (vor 10 Tagen).
PressRelease::factory()
->published()
->forPortal(Portal::Presseecho)
->for($category)
->for($company)
->create([
'title' => 'Meldung am Stichtag',
'slug' => 'meldung-am-stichtag',
'published_at' => now()->subDays(10)->setTime(9, 0),
]);
PressRelease::factory()
->published()
->forPortal(Portal::Presseecho)
->for($category)
->for($company)
->create([
'title' => 'Meldung vom Vortag',
'slug' => 'meldung-vom-vortag',
'published_at' => now()->subDays(11)->setTime(9, 0),
]);
LivewireVolt::test('web.press-release-feed', ['portal' => Portal::Presseecho->value, 'language' => 'de'])
->call('setTimeframe', 'today')
->assertSee('Meldung am Stichtag')
->assertDontSee('Meldung vom Vortag');
});