seed(RolesAndPermissionsSeeder::class); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); }); test('admin can render category create page', function () { /** @var TestCase $this */ $this->get(route('admin.categories.create')) ->assertSuccessful() ->assertSee('Kategorie anlegen'); }); test('admin can create a new category with both translations', function () { Livewire::test('admin.categories.create') ->set('portal', Portal::Both->value) ->set('nameDe', 'Künstliche Intelligenz') ->set('nameEn', 'Artificial Intelligence') ->set('descriptionDe', 'Themen rund um KI.') ->set('descriptionEn', 'Topics around AI.') ->call('save') ->assertHasNoErrors() ->assertRedirect(route('admin.categories.index')); $category = Category::query()->latest('id')->firstOrFail(); expect($category->portal)->toBe(Portal::Both); expect($category->is_active)->toBeTrue(); expect($category->translations()->count())->toBe(2); $de = $category->translations->firstWhere('locale', 'de'); $en = $category->translations->firstWhere('locale', 'en'); expect($de->name)->toBe('Künstliche Intelligenz'); expect($de->slug)->toBe('kunstliche-intelligenz'); expect($en->slug)->toBe('artificial-intelligence'); }); test('category create requires both names', function () { Livewire::test('admin.categories.create') ->set('nameDe', '') ->set('nameEn', '') ->call('save') ->assertHasErrors(['nameDe', 'nameEn']); }); test('category create generates unique slug per locale on collision', function () { $existing = Category::factory()->create(['portal' => Portal::Both->value]); $existing->translations()->createMany([ ['locale' => 'de', 'name' => 'Wirtschaft', 'slug' => 'wirtschaft'], ['locale' => 'en', 'name' => 'Business', 'slug' => 'business'], ]); Livewire::test('admin.categories.create') ->set('nameDe', 'Wirtschaft') ->set('nameEn', 'Business') ->call('save') ->assertHasNoErrors(); $newCategory = Category::query()->latest('id')->firstOrFail(); $de = $newCategory->translations->firstWhere('locale', 'de'); $en = $newCategory->translations->firstWhere('locale', 'en'); expect($de->slug)->toBe('wirtschaft-2'); expect($en->slug)->toBe('business-2'); }); test('admin can edit an existing category and rename slugs in place', function () { $category = Category::factory()->create(['portal' => Portal::Both->value]); $category->translations()->createMany([ ['locale' => 'de', 'name' => 'Alter Name', 'slug' => 'alter-name'], ['locale' => 'en', 'name' => 'Old Name', 'slug' => 'old-name'], ]); Livewire::test('admin.categories.edit', ['id' => $category->id]) ->set('nameDe', 'Neuer Name') ->set('slugDe', 'neuer-name') ->set('nameEn', 'New Name') ->set('slugEn', 'new-name') ->call('save') ->assertHasNoErrors(); $category->refresh()->load('translations'); $de = $category->translations->firstWhere('locale', 'de'); $en = $category->translations->firstWhere('locale', 'en'); expect($de->slug)->toBe('neuer-name'); expect($en->slug)->toBe('new-name'); }); test('category edit refuses parent assignment that would create a hierarchy loop', function () { $parent = Category::factory()->create(); $parent->translations()->createMany([ ['locale' => 'de', 'name' => 'Parent', 'slug' => 'parent'], ['locale' => 'en', 'name' => 'Parent', 'slug' => 'parent-en'], ]); $child = Category::factory()->create(['parent_id' => $parent->id]); $child->translations()->createMany([ ['locale' => 'de', 'name' => 'Child', 'slug' => 'child'], ['locale' => 'en', 'name' => 'Child', 'slug' => 'child-en'], ]); Livewire::test('admin.categories.edit', ['id' => $parent->id]) ->set('nameDe', 'Parent') ->set('slugDe', 'parent') ->set('nameEn', 'Parent') ->set('slugEn', 'parent-en') ->set('parentId', $child->id) ->call('save') ->assertHasErrors(['parentId']); }); test('category cannot be deleted while it has press releases', function () { $category = Category::factory()->create(); $category->translations()->createMany([ ['locale' => 'de', 'name' => 'Used', 'slug' => 'used'], ['locale' => 'en', 'name' => 'Used', 'slug' => 'used-en'], ]); PressRelease::factory()->create([ 'category_id' => $category->id, ]); Livewire::test('admin.categories.edit', ['id' => $category->id]) ->call('deleteCategory'); expect(Category::query()->find($category->id))->not->toBeNull(); }); test('category can be deleted when empty', function () { $category = Category::factory()->create(); $category->translations()->createMany([ ['locale' => 'de', 'name' => 'Empty', 'slug' => 'empty'], ['locale' => 'en', 'name' => 'Empty', 'slug' => 'empty-en'], ]); Livewire::test('admin.categories.edit', ['id' => $category->id]) ->call('deleteCategory') ->assertRedirect(route('admin.categories.index')); expect(Category::query()->find($category->id))->toBeNull(); }); test('deactivating a category clears the cached public pages and its feed at once', function () { /** @var TestCase $this */ $category = Category::factory()->create(['portal' => Portal::Presseecho->value, 'is_active' => true]); $category->translations()->createMany([ ['locale' => 'de', 'name' => 'Handel', 'slug' => 'handel'], ['locale' => 'en', 'name' => 'Trade', 'slug' => 'trade'], ]); PressRelease::factory()->published()->forPortal(Portal::Presseecho)->for($category) ->create(['language' => 'de', 'published_at' => now()->subHour()]); // Feed warm im Seiten-Cache. $this->get('https://presseecho.test/de/category/handel/feed')->assertSuccessful(); Livewire::test('admin.categories.edit', ['id' => $category->id]) ->set('isActive', false) ->call('save') ->assertHasNoErrors(); // Nicht erst nach Ablauf der Stale-Grenze: sofort weg. $this->get('https://presseecho.test/de/category/handel/feed')->assertNotFound(); }); test('creating an active category clears the cached public pages at once', function () { /** @var TestCase $this */ $generation = WebPageCache::generation(); Cache::put('web.llms.presseecho.test', 'alt', 3600); Livewire::test('admin.categories.create') ->set('portal', Portal::Presseecho->value) ->set('nameDe', 'Energie') ->set('nameEn', 'Energy') ->call('save') ->assertHasNoErrors(); // Umsetzungsaudit 29.09.2026, 5.2-Rest: Navigation und llms.txt zeigen // die neue Branche sofort, nicht erst nach bis zu einer Stunde. expect(WebPageCache::generation())->not->toBe($generation) ->and(Cache::has('web.llms.presseecho.test'))->toBeFalse(); });