seed(RolesAndPermissionsSeeder::class); }); function goneTestCompany(): Company { return Company::factory()->create([ 'portal' => Portal::Businessportal24, 'is_active' => true, ]); } function goneTestRelease(?Company $company = null): PressRelease { return PressRelease::factory() ->published() ->forPortal(Portal::Businessportal24) ->create([ 'company_id' => $company?->getKey(), 'published_at' => now()->subDay(), ]); } function goneTestAdmin(): User { $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); return $admin; } test('a press release marked gone answers 410 with a tombstone instead of 404', function () { /** @var TestCase $this */ $release = goneTestRelease(); $url = 'https://businessportal24.test/de/press-release/'.$release->slug; $this->get($url)->assertOk()->assertSeeText($release->title); app(PressReleaseService::class)->markGone($release); $this->get($url) ->assertStatus(410) ->assertSeeText('Diese Pressemitteilung wurde entfernt') ->assertDontSeeText($release->title) // Eigener Wortlaut: keine Depublikation "auf Wunsch des Absenders". ->assertDontSeeText('auf Wunsch des Absenders'); }); test('marking a press release gone archives it, blocks export and locks the re-sync', function () { /** @var TestCase $this */ $release = goneTestRelease(); app(PressReleaseService::class)->markGone($release); $release->refresh(); expect($release->gone_at)->not->toBeNull() ->and($release->status)->toBe(PressReleaseStatus::Archived) ->and($release->no_export)->toBeTrue() // Ohne Re-Sync-Sperre würde der nächste Importer-Lauf die Entfernung // wieder überschreiben. ->and($release->isSyncLocked())->toBeTrue() ->and($release->deleted_at)->toBeNull() ->and($release->statusLogs()->where('source', 'gone')->count())->toBe(1); }); test('a fully deleted press release still answers 404', function () { /** @var TestCase $this */ $release = goneTestRelease(); $url = 'https://businessportal24.test/de/press-release/'.$release->slug; $release->delete(); $this->get($url)->assertNotFound(); }); test('lifting the gone marker makes the press release publishable again', function () { /** @var TestCase $this */ $release = goneTestRelease(); $service = app(PressReleaseService::class); $service->markGone($release); $service->restoreFromGone($release->fresh()); $release->refresh(); expect($release->gone_at)->toBeNull() // Der Status bleibt bewusst archiviert — die Freigabe ist ein // eigener redaktioneller Schritt. ->and($release->status)->toBe(PressReleaseStatus::Archived); $this->get('https://businessportal24.test/de/press-release/'.$release->slug)->assertNotFound(); }); test('a newsroom marked gone answers 410 with a tombstone instead of 404', function () { /** @var TestCase $this */ $company = goneTestCompany(); goneTestRelease($company); $url = 'https://businessportal24.test/de/newsroom/'.$company->slug; $this->get($url)->assertOk(); app(CompanyRemovalService::class)->markGone($company); $this->get($url) ->assertStatus(410) ->assertSeeText('Dieser Newsroom wurde entfernt') ->assertDontSeeText($company->name); }); test('the newsroom tombstone beats a warm page cache', function () { /** @var TestCase $this */ $company = goneTestCompany(); goneTestRelease($company); $url = 'https://businessportal24.test/de/newsroom/'.$company->slug; // Erst aufwärmen, damit für den Slug ein Seiten-Cache existiert … $this->get($url)->assertOk(); // … dann den Marker OHNE Cache-Invalidierung setzen. Damit prüft der Test // die Absicherung in NewsroomDetailPageData (Gone-Prüfung vor dem Cache) // und nicht bloß das Verwerfen der Caches im Service. $company->forceFill(['gone_at' => now(), 'is_active' => false])->saveQuietly(); $this->get($url)->assertStatus(410); }); test('removing content drops the cached public listings immediately', function () { /** @var TestCase $this */ $release = goneTestRelease(); // Startseiten-/Ticker-Caches aufwärmen … $this->get('https://businessportal24.test/de')->assertOk(); app(PressReleaseService::class)->markGone($release); // … die entfernte Meldung darf danach in keiner Liste mehr auftauchen, // auch nicht aus dem Stale-Fenster (bis zu einer Stunde). $this->get('https://businessportal24.test/de') ->assertOk() ->assertDontSeeText($release->title); }); test('a fully deleted newsroom still answers 404', function () { /** @var TestCase $this */ $company = goneTestCompany(); goneTestRelease($company); $url = 'https://businessportal24.test/de/newsroom/'.$company->slug; // Erst aufwärmen: Die normale Löschung muss auch einen bereits // gecachten Newsroom sofort aus der Auslieferung nehmen — als 404, // nicht als Tombstone (F13). $this->get($url)->assertOk(); $company->delete(); $this->get($url) ->assertNotFound() ->assertDontSeeText('Dieser Newsroom wurde entfernt'); }); test('removing a newsroom leaves its press releases alone unless asked to cascade', function () { /** @var TestCase $this */ $company = goneTestCompany(); $release = goneTestRelease($company); app(CompanyRemovalService::class)->markGone($company); expect($release->fresh()->gone_at)->toBeNull(); $this->get('https://businessportal24.test/de/press-release/'.$release->slug)->assertOk(); }); test('removing a newsroom can cascade the 410 to its press releases', function () { /** @var TestCase $this */ $company = goneTestCompany(); $release = goneTestRelease($company); $affected = app(CompanyRemovalService::class)->markGone($company, includePressReleases: true); expect($affected)->toBe(1) ->and($release->fresh()->gone_at)->not->toBeNull(); $this->get('https://businessportal24.test/de/press-release/'.$release->slug)->assertStatus(410); }); test('cascading a newsroom removal flushes the page cache once, not per release', function () { /** @var TestCase $this */ $company = goneTestCompany(); goneTestRelease($company); goneTestRelease($company); goneTestRelease($company); $before = WebPageCache::generation(); $affected = app(CompanyRemovalService::class)->markGone($company, includePressReleases: true); // Ein Flush verwirft ALLE `web.`-Seiten-Caches auf einmal. Einer pro // Mitteilung wäre ein selbst ausgelöster Cache-Stampede: Eine Bestandsfirma // mit dreistellig vielen Meldungen zwänge die gesamte öffentliche Seite // ebenso oft in den Neuaufbau. expect($affected)->toBe(3) ->and(WebPageCache::generation())->toBe($before + 1); }); test('lifting a newsroom removal keeps individually removed press releases gone', function () { /** @var TestCase $this */ $company = goneTestCompany(); $release = goneTestRelease($company); $removal = app(CompanyRemovalService::class); $removal->markGone($company, includePressReleases: true); $removal->restoreFromGone($company->fresh()); $company->refresh(); expect($company->gone_at)->toBeNull() ->and($company->is_active)->toBeTrue() // Eine einmal entfernte PM kommt nicht automatisch zurück — sonst // könnte eine rechtlich veranlasste Einzel-Entfernung versehentlich // mit aufgehoben werden. ->and($release->fresh()->gone_at)->not->toBeNull(); }); test('lifting a newsroom removal leaves an already inactive newsroom inactive', function () { /** @var TestCase $this */ $company = Company::factory()->create([ 'portal' => Portal::Businessportal24, 'is_active' => false, ]); $removal = app(CompanyRemovalService::class); $removal->markGone($company); $removal->restoreFromGone($company->fresh()); $company->refresh(); // Die Oberfläche verspricht Umkehrbarkeit. Eine Firma, die schon vor der // Entfernung deaktiviert war, darf durch das Aufheben nicht öffentlich // werden — sie stünde sonst plötzlich in Newsroom-Übersicht und Sitemap. expect($company->gone_at)->toBeNull() ->and($company->is_active)->toBeFalse() ->and($company->is_active_before_gone)->toBeNull(); }); test('lifting a press release removal restores the export flag it overwrote', function () { /** @var TestCase $this */ $release = goneTestRelease(); $release->update(['no_export' => false]); $service = app(PressReleaseService::class); $service->markGone($release); expect($release->fresh()->no_export)->toBeTrue(); $service->restoreFromGone($release->fresh()); // `no_export` gehört zur Entfernung, nicht zur Mitteilung — sonst stünde // sie in Kunden- und Admin-Ansicht dauerhaft als „kein Export“ da, ohne // dass das je jemand gewählt hätte. expect($release->fresh()->no_export)->toBeFalse() ->and($release->fresh()->no_export_before_gone)->toBeNull(); }); test('lifting a press release removal keeps an export block that predates it', function () { /** @var TestCase $this */ $release = goneTestRelease(); $release->update(['no_export' => true]); $service = app(PressReleaseService::class); $service->markGone($release); $service->restoreFromGone($release->fresh()); // Umgekehrt gilt dasselbe: Eine schon vorher gesetzte Sperre bleibt. expect($release->fresh()->no_export)->toBeTrue(); }); test('a press release marked gone disappears from listings and the sitemap', function () { /** @var TestCase $this */ $company = goneTestCompany(); $release = goneTestRelease($company); $this->get('https://businessportal24.test/sitemap.xml')->assertSee($release->slug, false); app(PressReleaseService::class)->markGone($release); $this->get('https://businessportal24.test/sitemap.xml')->assertDontSee($release->slug, false); $this->get('https://businessportal24.test/de/newsrooms')->assertDontSeeText($company->name); }); test('admin can remove a press release with 410 from the edit page', function () { /** @var TestCase $this */ $release = goneTestRelease(); $this->actingAs(goneTestAdmin()); Livewire::test('admin.press-releases.edit', ['id' => $release->id]) ->call('markGone') ->assertRedirect(route('admin.press-releases.index')); expect($release->fresh()->gone_at)->not->toBeNull(); }); test('admin can remove a newsroom with 410 from the edit page', function () { /** @var TestCase $this */ $company = goneTestCompany(); $release = goneTestRelease($company); $this->actingAs(goneTestAdmin()); Livewire::test('admin.companies.edit', ['id' => $company->id]) ->set('goneIncludesPressReleases', true) ->call('markGone') ->assertRedirect(route('admin.companies.index')); $company->refresh(); expect($company->gone_at)->not->toBeNull() ->and($company->is_active)->toBeFalse() ->and($release->fresh()->gone_at)->not->toBeNull(); }); test('admin can lift a press release removal from the edit page', function () { /** @var TestCase $this */ $release = goneTestRelease(); app(PressReleaseService::class)->markGone($release); $this->actingAs(goneTestAdmin()); Livewire::test('admin.press-releases.edit', ['id' => $release->id]) ->assertSee('410-Entfernung aufheben') ->call('restoreGone') ->assertRedirect(route('admin.press-releases.edit', $release->id)); expect($release->fresh()->gone_at)->toBeNull(); }); test('admin can lift a newsroom removal from the edit page', function () { /** @var TestCase $this */ $company = goneTestCompany(); app(CompanyRemovalService::class)->markGone($company); $this->actingAs(goneTestAdmin()); Livewire::test('admin.companies.edit', ['id' => $company->id]) ->assertSee('410-Entfernung aufheben') ->call('restoreGone') ->assertRedirect(route('admin.companies.edit', $company->id)); $company->refresh(); expect($company->gone_at)->toBeNull() ->and($company->is_active)->toBeTrue(); });