create(['portal' => $portal]); $category->translations()->create([ 'locale' => 'de', 'name' => 'Energie & Klima', 'slug' => 'energie-klima', ]); $company = Company::factory()->create([ 'portal' => $portal, 'name' => 'Muster Energie GmbH', 'is_active' => true, 'website' => 'https://muster-energie.de', 'boilerplate' => 'Die Muster Energie GmbH ist ein Spezialist für Industrie-Dekarbonisierung.', ]); return [$category, $company]; } test('businessportal24 release detail renders the editorial shell with real data', function () { /** @var TestCase $this */ [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); $release = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create([ 'title' => 'Energiewende 2030: 12-Milliarden-Programm angekündigt', 'subtitle' => 'Erste Tranche fließt ab dem dritten Quartal 2026.', 'slug' => 'energiewende-2030-programm', 'text' => '

Berlin. Das Programm beschleunigt die Dekarbonisierung der Schwerindustrie.

', 'keywords' => 'Energiewende, Dekarbonisierung, Wasserstoff', 'published_at' => now()->subDay(), ]); $contact = Contact::factory()->create([ 'portal' => Portal::Businessportal24, 'company_id' => $company->id, 'first_name' => 'Annika', 'last_name' => 'Werthmann', 'email' => 'presse@muster-energie.de', ]); $release->contacts()->attach($contact); $this->get('https://businessportal24.test/de/press-release/'.$release->slug) ->assertSuccessful() ->assertSee('businessportal', false) ->assertSeeText('Energiewende 2030: 12-Milliarden-Programm angekündigt') ->assertSeeText('Erste Tranche fließt ab dem dritten Quartal 2026.') ->assertSeeText('Muster Energie GmbH') ->assertSeeText('Energie & Klima') ->assertSeeText('Pressekontakt') ->assertSeeText('Annika Werthmann') ->assertSeeText('Dekarbonisierung der Schwerindustrie') ->assertSeeText('Über das Unternehmen') ->assertSeeText('Wasserstoff') // Magic-Link-Hinweis: hinterlegte Kontakte verwalten die PM im pressekonto. ->assertSeeText('Sie sind als Firma oder Pressekontakt dieser Mitteilung hinterlegt?') ->assertSee(rtrim(config('domains.domain_portal_url'), '/').'/anmeldelink', false) // Verwaltungsblock (1b.4): maskierte Empfängeradresse, Button zur // Claim-Seite — im Text UND in der Seitenleiste, nie die volle Adresse. ->assertSeeText('Anmeldelink anfordern') ->assertSee('/de/newsroom/'.$company->slug.'/verwalten', false) ->assertSeeText(NewsroomClaimService::mask((string) $company->email)) ->assertDontSeeText((string) $company->email); }); test('release detail falls back to the company contact when the release has none', function () { /** @var TestCase $this */ [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); // Legacy-Import-Fall: PM ohne eigene Kontakt-Verknüpfung — im Altsystem // hingen Pressekontakte an der Firma. $release = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create(['published_at' => now()->subDay()]); Contact::factory()->create([ 'portal' => Portal::Businessportal24, 'company_id' => $company->id, 'first_name' => 'Jonas', 'last_name' => 'Feldkamp', 'email' => 'presse@muster-energie.de', ]); expect($release->contacts()->count())->toBe(0); $this->get('https://businessportal24.test/de/press-release/'.$release->slug) ->assertSuccessful() ->assertSeeText('Pressekontakt') ->assertSeeText('Jonas Feldkamp'); // Der Fallback ist reine Anzeige — es entsteht keine Pivot-Verknüpfung. expect($release->contacts()->count())->toBe(0); }); test('release detail shows company data as press contact when no contact record exists at all', function () { /** @var TestCase $this */ [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); $company->update(['email' => 'kontakt@muster-energie.de']); $release = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create(['published_at' => now()->subDay()]); $this->get('https://businessportal24.test/de/press-release/'.$release->slug) ->assertSuccessful() ->assertSeeText('Pressekontakt') ->assertSeeText('kontakt@muster-energie.de'); }); test('release detail without image embeds the headline in the generated hero', function () { /** @var TestCase $this */ [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); $release = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create([ 'title' => 'Meldung ohne Pressefoto', 'slug' => 'meldung-ohne-pressefoto', 'placeholder_variant' => PressReleasePlaceholder::LinesGreen, ]); $this->get('https://businessportal24.test/de/press-release/'.$release->slug) ->assertSuccessful() ->assertSeeText('Meldung ohne Pressefoto') ->assertSee('bg-hero-grad', false) // Es muss dieselbe Platzhalter-Variante wie im User-Panel erscheinen. ->assertSee('images/press-release-placeholders/05-lines-green.svg', false) ->assertDontSee('Bild · Pressefoto'); }); test('release detail without chosen placeholder falls back to the generic hero', function () { /** @var TestCase $this */ // Altbestand: PMs ohne gesetzte `placeholder_variant` rendern weiterhin // die generische Hero-Fallback-Grafik statt einer Platzhalter-SVG. [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); $release = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create([ 'title' => 'Altbestand ohne Platzhalter-Variante', 'slug' => 'altbestand-ohne-variante', ]); PressRelease::withoutGlobalScopes()->whereKey($release->id)->update(['placeholder_variant' => null]); $this->get('https://businessportal24.test/de/press-release/'.$release->slug) ->assertSuccessful() ->assertSeeText('Altbestand ohne Platzhalter-Variante') ->assertDontSee('images/press-release-placeholders/', false); }); test('release detail shows the ad slot self-promotion inline', function () { /** @var TestCase $this */ // Werbeplatz-Hinweis (Eigenwerbung): Klick öffnet das Kontakt-Widget // mit vorgewähltem Thema „Werbeanfrage". [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); $release = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create(['slug' => 'meldung-mit-werbeplatz', 'published_at' => now()->subDay()]); $this->get('https://businessportal24.test/de/press-release/'.$release->slug) ->assertSuccessful() ->assertSeeText('Hier könnte Ihre Werbung stehen') ->assertSeeText('Jetzt Werbeplatz anfragen') ->assertSee("{ topic: 'advertising' }", false); }); test('release detail shows prominent report links below the article', function () { /** @var TestCase $this */ // T13 (Meldeverfahren): Die Melde-Links stehen sichtbar unter jeder // Mitteilung — nicht nur in der Sidebar-Box „Weiterführend". [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); $release = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create(['published_at' => now()->subDay()]); $this->get('https://businessportal24.test/de/press-release/'.$release->slug) ->assertSuccessful() ->assertSeeText('Sie haben ein rechtliches Anliegen zu dieser Mitteilung?') ->assertSeeText('Inhalt melden') ->assertSeeText('Eigene Daten entfernen') ->assertSeeText('Meldungen sind kostenlos.') ->assertSee(route('legal-request.create', ['slug' => $release->slug, 'type' => 'report']), false) ->assertSee(route('legal-request.create', ['slug' => $release->slug, 'type' => 'dsgvo']), false); }); test('release detail attributes content submitted via a distribution agency', function () { /** @var TestCase $this */ // Legacy-Konvention: PMs, die über einen Verteiler-Account eingeliefert // wurden (z. B. connektar via API), tragen die Zeile „Original-Inhalt // von {Firma} und übermittelt von {Übermittler}". Der Übermittler kommt // aus dem Profilfeld agency_name des einliefernden Accounts. [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); $agencyUser = User::factory()->withAgency('connektar.de Presseverteiler')->create(); $release = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->for($agencyUser, 'user') ->create(['slug' => 'api-eingelieferte-meldung', 'published_at' => now()->subDay()]); $this->get('https://businessportal24.test/de/press-release/'.$release->slug) ->assertSuccessful() ->assertSeeText('Original-Inhalt von Muster Energie GmbH und übermittelt von connektar.de Presseverteiler'); }); test('release detail attribution collapses when agency and company share the same name', function () { /** @var TestCase $this */ [$category] = detailCategoryAndCompany(Portal::Businessportal24); $agencyUser = User::factory()->withAgency('PR-Werk GmbH')->create(); $sameNameCompany = Company::factory()->create([ 'portal' => Portal::Businessportal24, 'type' => CompanyType::Company, 'name' => 'PR-Werk GmbH', 'is_active' => true, ]); $release = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($sameNameCompany) ->for($agencyUser, 'user') ->create(['slug' => 'eigene-agentur-meldung', 'published_at' => now()->subDay()]); $this->get('https://businessportal24.test/de/press-release/'.$release->slug) ->assertSuccessful() ->assertSeeText('Inhalt übermittelt von PR-Werk GmbH') ->assertDontSeeText('Original-Inhalt von PR-Werk GmbH'); }); test('release detail attributes the company itself without a distribution agency', function () { /** @var TestCase $this */ [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); $release = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create(['slug' => 'direkt-eingestellte-meldung', 'published_at' => now()->subDay()]); $this->get('https://businessportal24.test/de/press-release/'.$release->slug) ->assertSuccessful() ->assertSeeText('Original-Inhalt von Muster Energie GmbH') ->assertDontSeeText('übermittelt von'); }); test('release detail shows the ai label only for ai-generated declarations', function () { /** @var TestCase $this */ // Art. 50 KI-VO: Option 2 (ganz/überwiegend KI) trägt das Label // „KI-generiert" am Anfang des Inhalts, Option 1 bleibt ohne Label. // KI-deklarierte Bilder tragen das Badge „KI-generiert" direkt am Bild. [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); $aiRelease = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create([ 'slug' => 'ki-generierte-meldung', 'ai_text_declaration' => AiTextDeclaration::MostlyAi->value, 'published_at' => now()->subDay(), ]); $aiRelease->images()->create([ 'disk' => 'public', 'path' => 'press-releases/'.$aiRelease->id.'/images/title.jpg', 'author' => 'Beispiel GmbH', 'license_type' => 'ai_generated', 'rights_confirmed_at' => now(), 'is_ai_generated' => true, 'ai_declaration' => AiImageDeclaration::AiGenerated->value, 'is_preview' => true, 'sort_order' => 1, ]); $this->get('https://businessportal24.test/de/press-release/'.$aiRelease->slug) ->assertSuccessful() ->assertSeeText('KI-generiert: Nach Angaben des einreichenden Unternehmens wurde diese Pressemitteilung ganz oder überwiegend mit generativer KI erstellt.') // Badge am Bild — Kurzform, nicht mehr „KI-generierte Illustration". ->assertSeeText('KI-generiert') ->assertDontSeeText('KI-generierte Illustration'); $plainRelease = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create([ 'slug' => 'meldung-ohne-ki', 'ai_text_declaration' => AiTextDeclaration::None->value, 'published_at' => now()->subDay(), ]); $this->get('https://businessportal24.test/de/press-release/'.$plainRelease->slug) ->assertSuccessful() ->assertDontSeeText('KI-generiert'); }); test('release detail counts a hit', function () { /** @var TestCase $this */ [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); $release = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create(['slug' => 'hit-zaehler', 'hits' => 4, 'published_at' => now()->subDay()]); $this->get('https://businessportal24.test/de/press-release/'.$release->slug)->assertSuccessful(); // Der Aufruf landet im Ereignis-Log; in den Gesamtzähler verbucht ihn der // Scheduler, damit der Request die große Tabelle nicht beschreiben muss. $this->artisan('press-releases:aggregate-views')->assertSuccessful(); expect(PressRelease::withoutGlobalScopes()->find($release->id)->hits)->toBe(5); }); test('release detail returns 404 for drafts and unknown slugs', function () { /** @var TestCase $this */ [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); $draft = PressRelease::factory() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create([ 'status' => PressReleaseStatus::Draft, 'slug' => 'noch-ein-entwurf', 'published_at' => now()->subDay(), ]); $this->get('https://businessportal24.test/de/press-release/'.$draft->slug)->assertNotFound(); $this->get('https://businessportal24.test/de/press-release/gibt-es-nicht')->assertNotFound(); }); test('release detail does not expose releases from another portal', function () { /** @var TestCase $this */ [$category, $company] = detailCategoryAndCompany(Portal::Presseecho); $release = PressRelease::factory() ->published() ->forPortal(Portal::Presseecho) ->for($category) ->for($company) ->create([ 'title' => 'Nur für presseecho sichtbar', 'slug' => 'nur-presseecho', 'published_at' => now()->subDay(), ]); $this->get('https://businessportal24.test/de/press-release/'.$release->slug)->assertNotFound(); $this->get('https://presseecho.test/de/press-release/'.$release->slug) ->assertSuccessful() ->assertSee('presse', false) ->assertSeeText('Nur für presseecho sichtbar'); }); test('release detail shows more from newsroom and related releases', function () { /** @var TestCase $this */ [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); $main = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create(['title' => 'Hauptmeldung Energie', 'slug' => 'hauptmeldung-energie', 'published_at' => now()->subDay()]); PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create(['title' => 'Weitere Meldung desselben Newsrooms', 'slug' => 'weitere-newsroom-meldung', 'published_at' => now()->subDays(2)]); $otherCompany = Company::factory()->create([ 'portal' => Portal::Businessportal24, 'name' => 'Andere Industrie AG', 'is_active' => true, ]); PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($otherCompany) ->create(['title' => 'Verwandte Branchenmeldung', 'slug' => 'verwandte-branchenmeldung', 'published_at' => now()->subDays(3)]); $this->get('https://businessportal24.test/de/press-release/'.$main->slug) ->assertSuccessful() ->assertSeeText('Mehr von Muster Energie GmbH') ->assertSeeText('Weitere Meldung desselben Newsrooms') ->assertSeeText('Verwandte Meldungen') ->assertSeeText('Verwandte Branchenmeldung') ->assertSee('grid grid-cols-1 gap-10 md:grid-cols-2', false); }); test('a single recommendation list spans the full layout width', function () { /** @var TestCase $this */ [$category, $company] = detailCategoryAndCompany(Portal::Businessportal24); $main = PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($company) ->create(['slug' => 'einzelliste-hauptmeldung', 'published_at' => now()->subDay()]); $otherCompany = Company::factory()->create([ 'portal' => Portal::Businessportal24, 'name' => 'Andere Industrie AG', 'is_active' => true, ]); // Nur verwandte Meldungen (andere Firma, gleiche Rubrik) — kein // "Mehr von …": die einzelne Liste darf nicht auf halber Breite stehen. PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->for($category) ->for($otherCompany) ->create(['slug' => 'einzelliste-verwandt', 'published_at' => now()->subDays(2)]); $this->get('https://businessportal24.test/de/press-release/'.$main->slug) ->assertSuccessful() ->assertSeeText('Verwandte Meldungen') ->assertSee('grid grid-cols-1 gap-10"', false) ->assertDontSee('grid grid-cols-1 gap-10 md:grid-cols-2', false); });