- R-02/K-03: Aufrufaggregation und Merge ändern updated_at nicht mehr; neue Spalte press_releases.content_changed_at (INSTANT), contentModifiedAt() für Sitemap-lastmod, dateModified und article:modified_time - R-03: Sitemap und Legacy-view_url mit /en/ für englische Meldungen - R-04/O01: X-Pk-Ref je Empfänger per HMAC signiert; auf eine Sperre zählt — hart wie weich — nur ein gültig signierter Rückläufer, unsignierte standardmäßig nur zur Sichtung (BOUNCE_UNSIGNED_KIND=policy) - R-05/R-06: Unmerge setzt nur unveränderte Füllwerte zurück (company_merge_moves.merged_value), Canonical-Neubewertung nach Merge und Unmerge, assignAll setzt veraltete Verweise zurück - R-07 bis R-11: Crawler zählen nicht als Aufruf, Mail-Klick gedrosselt und verzögert geschrieben, Claim-Zähler erst nach Versand, email_bounces nach 395 Tagen gelöscht, Canonical-Portal über Kategorie-ID, llms-Cache geleert, Feed-Teaser ohne Doppel-Escaping, Merge-Ketten aufgelöst - N17/N18: kein falsches „gelistet seit", Rollen im Indexbericht wie Sitemap - OPT-1/OPT-2: topFeeds in SQL, purge-events löscht nach Grenz-ID Enthält aus PressRelease.php auch die Card-Bild-Helfer (B-01) und wasEverPublished()/neverPublished() (A-05), weil die Datei ungeteilt committet ist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
198 lines
7.3 KiB
PHP
198 lines
7.3 KiB
PHP
<?php
|
|
|
|
use App\Enums\Portal;
|
|
use App\Models\Category;
|
|
use App\Models\PressRelease;
|
|
use App\Models\User;
|
|
use App\Support\WebPageCache;
|
|
use Database\Seeders\RolesAndPermissionsSeeder;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Livewire\Volt\Volt as LivewireVolt;
|
|
use Tests\TestCase;
|
|
|
|
beforeEach(function (): void {
|
|
/** @var TestCase $this */
|
|
$this->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 () {
|
|
LivewireVolt::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 () {
|
|
LivewireVolt::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'],
|
|
]);
|
|
|
|
LivewireVolt::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'],
|
|
]);
|
|
|
|
LivewireVolt::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'],
|
|
]);
|
|
|
|
LivewireVolt::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,
|
|
]);
|
|
|
|
LivewireVolt::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'],
|
|
]);
|
|
|
|
LivewireVolt::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();
|
|
|
|
LivewireVolt::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);
|
|
|
|
LivewireVolt::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();
|
|
});
|