presseportale/tests/Feature/Web/FeedTest.php
Kevin Adametz ff75159b8f web: RSS-Feed je Newsroom, RSS-Übersichtsseite, „RSS" in der Topleiste (Roadmap 3.1, Nachtrag)
- /{edition}/newsroom/{slug}/feed: Sichtbarkeit wie die Newsroom-Seite
  (aktiv, nicht entfernt, mindestens eine Meldung), Prüfung vor dem Cache,
  301 bei zusammengeführter Firma; Autodiscovery und Link „RSS-Feed" auf
  der Newsroom-Seite; Hub leitet auf das Portal der Firma.
- /{edition}/rss (RssPageController, static.rss DE/EN): Was RSS ist, wie
  ein Reader funktioniert, Hauptfeed, alle Branchen-Feeds mit Adresse,
  Muster des Newsroom-Feeds, Hinweise. Portalseite mit Portal-Kontext,
  Hub → presseecho, in Sitemap und llms.txt.
- Topleiste, Burger-Menü und Footer verlinken RSS neben FAQ/Hilfe/API.
- FeedController in portal/category/newsroom aufgeteilt; Panel
  „Auslieferung" nennt zusätzlich den Newsroom-Feed.
- Tests: FeedTest (+3), RssPageTest (4), StaticPagesTest, VisibilityStatsTest.
- dev/2026-09-01: Nachtrag in Protokoll Teil 11 und Roadmap 3.1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-21 08:27:14 +00:00

227 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Enums\Portal;
use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use App\Support\WebCaches;
use App\Support\WebPageCache;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
/**
* RSS-Feeds je Branche und je Portal (Roadmap 3.1): /{edition}/feed und
* /{edition}/category/{slug}/feed.
*/
function feedCategory(Portal $portal = Portal::Presseecho, string $slug = 'handel', string $name = 'Handel', ?Category $parent = null): Category
{
$category = Category::factory()->create(['portal' => $portal, 'is_active' => true, 'parent_id' => $parent?->id]);
$category->translations()->create(['locale' => 'de', 'name' => $name, 'slug' => $slug, 'description' => 'Meldungen aus dem Bereich '.$name.'.']);
$category->translations()->create(['locale' => 'en', 'name' => $name.' EN', 'slug' => $slug.'-en']);
return $category;
}
function feedRelease(Category $category, string $slug, array $attributes = [], Portal $portal = Portal::Presseecho): PressRelease
{
return PressRelease::factory()
->published()
->forPortal($portal)
->for($category)
->for(Company::factory()->presseecho()->create(['name' => 'Feed GmbH & Co.']))
->create(['slug' => $slug, 'published_at' => now()->subHour(), 'language' => 'de', ...$attributes]);
}
test('the category feed lists the newest releases of the category and its children on the own portal', function () {
/** @var TestCase $this */
$category = feedCategory();
$child = feedCategory(Portal::Presseecho, 'einzelhandel', 'Einzelhandel', $category);
$other = feedCategory(Portal::Presseecho, 'energie', 'Energie');
feedRelease($category, 'neu', ['title' => 'Neue Meldung & mehr', 'text' => '<p>Erster Absatz der Meldung.</p>', 'published_at' => now()->subHour()]);
feedRelease($child, 'kind', ['title' => 'Aus der Unterkategorie', 'published_at' => now()->subHours(2)]);
feedRelease($other, 'andere-branche');
feedRelease($category, 'fremdes-portal', [], Portal::Businessportal24);
feedRelease($category, 'terminiert', ['published_at' => now()->addHour()]);
feedRelease($category, 'englisch', ['language' => 'en']);
feedRelease($category, 'beide-portale', ['published_at' => now()->subHours(3)], Portal::Both);
$response = $this->get('https://presseecho.test/de/category/handel/feed')->assertSuccessful();
expect($response->headers->get('Content-Type'))->toBe('application/rss+xml; charset=UTF-8');
$xml = (string) $response->getContent();
expect($xml)
->toStartWith('<?xml version="1.0" encoding="UTF-8"?>')
->toContain('<title>Pressemitteilungen Handel – presseecho</title>')
->toContain('<link>https://presseecho.test/de/category/handel</link>')
->toContain('<atom:link href="https://presseecho.test/de/category/handel/feed" rel="self" type="application/rss+xml" />')
->toContain('<description>Meldungen aus dem Bereich Handel.</description>')
->toContain('<language>de</language>')
->toContain('<title>Neue Meldung &amp; mehr</title>')
->toContain('<guid isPermaLink="true">https://presseecho.test/de/press-release/neu</guid>')
->toContain('<description>Erster Absatz der Meldung.</description>')
->toContain('<category>Handel</category>')
->toContain('<dc:creator>Feed GmbH &amp; Co.</dc:creator>')
->toContain('<link>https://presseecho.test/de/press-release/kind</link>')
->toContain('<link>https://presseecho.test/de/press-release/beide-portale</link>');
// Neueste zuerst.
expect(strpos($xml, 'press-release/neu'))->toBeLessThan((int) strpos($xml, 'press-release/kind'));
foreach (['andere-branche', 'fremdes-portal', 'terminiert', 'press-release/englisch'] as $absent) {
$this->assertStringNotContainsString($absent, $xml);
}
// Wohlgeformt: die Ausgabe muss ein XML-Parser lesen können.
$document = simplexml_load_string($xml);
expect($document)->not->toBeFalse()
->and(count($document->channel->item))->toBe(3);
});
test('the portal feed lists releases of all categories, newest first, capped', function () {
/** @var TestCase $this */
$handel = feedCategory();
$energie = feedCategory(Portal::Presseecho, 'energie', 'Energie');
feedRelease($handel, 'handel-neu', ['published_at' => now()->subHour()]);
feedRelease($energie, 'energie-alt', ['published_at' => now()->subDay()]);
feedRelease($handel, 'fremd', [], Portal::Businessportal24);
$xml = (string) $this->get('https://presseecho.test/de/feed')->assertSuccessful()->getContent();
expect($xml)
->toContain('<title>Pressemitteilungen – presseecho</title>')
->toContain('<link>https://presseecho.test/de</link>')
->toContain('<atom:link href="https://presseecho.test/de/feed" rel="self" type="application/rss+xml" />')
->toContain('press-release/handel-neu')
->toContain('press-release/energie-alt');
expect(strpos($xml, 'handel-neu'))->toBeLessThan((int) strpos($xml, 'energie-alt'));
$this->assertStringNotContainsString('press-release/fremd', $xml);
});
test('an unknown or inactive category has no feed', function () {
/** @var TestCase $this */
$inactive = feedCategory(Portal::Presseecho, 'veraltet', 'Veraltet');
$inactive->update(['is_active' => false]);
$this->get('https://presseecho.test/de/category/gibt-es-nicht/feed')->assertNotFound();
$this->get('https://presseecho.test/de/category/veraltet/feed')->assertNotFound();
});
test('the english edition serves the feed under the english slug with english releases only', function () {
/** @var TestCase $this */
$category = feedCategory();
feedRelease($category, 'deutsch');
feedRelease($category, 'english', ['language' => 'en', 'title' => 'English release']);
$xml = (string) $this->get('https://presseecho.test/en/category/handel-en/feed')->assertSuccessful()->getContent();
expect($xml)
->toContain('<title>Handel EN press releases – presseecho</title>')
->toContain('<language>en</language>')
->toContain('https://presseecho.test/en/press-release/english');
$this->assertStringNotContainsString('press-release/deutsch', $xml);
});
test('category and home pages announce the feeds for autodiscovery and link them', function () {
/** @var TestCase $this */
$category = feedCategory();
feedRelease($category, 'irgendwas');
$this->get('https://presseecho.test/de/category/handel')->assertSuccessful()
->assertSee('<link rel="alternate" type="application/rss+xml" title="RSS-Feed: Pressemitteilungen Handel – presseecho" href="https://presseecho.test/de/category/handel/feed">', false)
->assertSee('<link rel="alternate" type="application/rss+xml" title="RSS-Feed: alle Pressemitteilungen – presseecho" href="https://presseecho.test/de/feed">', false)
->assertSee('href="https://presseecho.test/de/category/handel/feed" type="application/rss+xml"', false);
$this->get('https://presseecho.test/de')->assertSuccessful()
->assertSee('type="application/rss+xml" title="RSS-Feed: alle Pressemitteilungen – presseecho" href="https://presseecho.test/de/feed"', false);
$this->get('https://presseecho.test/llms.txt')->assertSuccessful()
->assertSee('https://presseecho.test/de/feed', false)
->assertSee('https://presseecho.test/de/category/{slug}/feed', false)
->assertSee('https://presseecho.test/de/newsroom/{slug}/feed', false)
->assertSee('https://presseecho.test/de/rss', false);
});
test('the hub redirects feed addresses to the portal', function () {
/** @var TestCase $this */
feedCategory(Portal::Both);
$company = Company::factory()->businessportal24()->create(['slug' => 'nordwind', 'is_active' => true]);
$this->get('https://pressekonto.test/de/feed')
->assertRedirect('https://presseecho.test/de/feed')
->assertStatus(301);
$this->get('https://pressekonto.test/de/category/handel/feed')
->assertRedirect('https://presseecho.test/de/category/handel/feed')
->assertStatus(301);
// Newsroom-Feed folgt dem Portal der Firma, nicht dem Standard-Portal.
$this->get('https://pressekonto.test/de/newsroom/nordwind/feed')
->assertRedirect('https://businessportal24.test/de/newsroom/nordwind/feed')
->assertStatus(301);
$this->get('https://pressekonto.test/de/newsroom/gibt-es-nicht/feed')->assertNotFound();
});
test('the newsroom feed lists the releases of one company on the own portal and follows the newsroom visibility', function () {
/** @var TestCase $this */
$category = feedCategory();
$company = Company::factory()->presseecho()->create(['name' => 'Nordwind Energie AG', 'slug' => 'nordwind', 'is_active' => true, 'boilerplate' => '<p>Nordwind baut Windparks.</p>']);
$other = Company::factory()->presseecho()->create(['slug' => 'andere', 'is_active' => true]);
$mine = fn (string $slug, array $attributes = []) => PressRelease::factory()->published()->forPortal(Portal::Presseecho)->for($category)->for($company)
->create(['slug' => $slug, 'language' => 'de', 'published_at' => now()->subHour(), ...$attributes]);
$mine('nordwind-neu', ['title' => 'Nordwind eröffnet Windpark']);
$mine('nordwind-alt', ['published_at' => now()->subDays(2)]);
$mine('nordwind-en', ['language' => 'en']);
PressRelease::factory()->published()->forPortal(Portal::Presseecho)->for($category)->for($other)->create(['slug' => 'andere-firma', 'language' => 'de', 'published_at' => now()->subHour()]);
$response = $this->get('https://presseecho.test/de/newsroom/nordwind/feed')->assertSuccessful();
$xml = (string) $response->getContent();
expect($xml)
->toContain('<title>Pressemitteilungen von Nordwind Energie AG – presseecho</title>')
->toContain('<link>https://presseecho.test/de/newsroom/nordwind</link>')
->toContain('<atom:link href="https://presseecho.test/de/newsroom/nordwind/feed" rel="self" type="application/rss+xml" />')
->toContain('<description>Nordwind baut Windparks.</description>')
->toContain('press-release/nordwind-neu')
->toContain('press-release/nordwind-alt');
$this->assertStringNotContainsString('andere-firma', $xml);
$this->assertStringNotContainsString('nordwind-en', $xml);
// Ohne veröffentlichte Meldung keine öffentliche Seite — und kein Feed.
$this->get('https://presseecho.test/de/newsroom/andere/feed')->assertSuccessful();
$empty = Company::factory()->presseecho()->create(['slug' => 'leer', 'is_active' => true]);
$this->get('https://presseecho.test/de/newsroom/leer/feed')->assertNotFound();
// Deaktiviert oder entfernt: sofort weg, auch mit warmem Cache.
$company->update(['is_active' => false]);
$this->get('https://presseecho.test/de/newsroom/nordwind/feed')->assertNotFound();
$company->update(['is_active' => true, 'gone_at' => now()]);
$this->get('https://presseecho.test/de/newsroom/nordwind/feed')->assertNotFound();
});
test('the newsroom page announces its feed for autodiscovery and links it', function () {
/** @var TestCase $this */
$category = feedCategory();
$company = Company::factory()->presseecho()->create(['name' => 'Nordwind Energie AG', 'slug' => 'nordwind', 'is_active' => true]);
PressRelease::factory()->published()->forPortal(Portal::Presseecho)->for($category)->for($company)->create(['slug' => 'nordwind-neu', 'language' => 'de', 'published_at' => now()->subHour()]);
$this->get('https://presseecho.test/de/newsroom/nordwind')->assertSuccessful()
->assertSee('<link rel="alternate" type="application/rss+xml" title="RSS-Feed: Pressemitteilungen von Nordwind Energie AG – presseecho" href="https://presseecho.test/de/newsroom/nordwind/feed">', false)
->assertSee('href="https://presseecho.test/de/newsroom/nordwind/feed" type="application/rss+xml"', false)
->assertSeeText('RSS-Feed');
});
test('the feed is cached with the page data and cleared with the public pages', function () {
/** @var TestCase $this */
$category = feedCategory();
feedRelease($category, 'erste');
$this->get('https://presseecho.test/de/category/handel/feed')->assertSuccessful();
$key = WebPageCache::key('feed', ['presseecho', 'de', 'category', 'handel']);
expect(Cache::has($key))->toBeTrue();
WebCaches::forgetPublicPages();
expect(Cache::has(WebPageCache::key('feed', ['presseecho', 'de', 'category', 'handel'])))->toBeFalse();
});