Block 3 — Sichtbarkeit, Löschung, Cache, Eingaben F04: withoutGlobalScopes() ohne Argument entfernte neben dem PortalScope auch den Soft-Delete-Filter. Öffentliche, Kunden-, API-, Vorschau- und Medienpfade sowie die PressRelease-Helfer schreiben jetzt withoutGlobalScope(PortalScope::class). Zwei Stellen, die der Audit nicht kannte: die öffentliche Meldungsseite zeigte gelöschte Firmenkontakte mit Name, Mail und Telefon; das Pressefach listete gelöschte Kontakte weiter und ließ sie bearbeiten. Ein Architekturtest friert die 39 bewusst verbliebenen Stellen (Admin, Importer, Befehle, Slug-Eindeutigkeit) ein. Der Vorschau-Test prüfte bisher forceDelete statt delete. F22: LOWER(email) machte auf companies (110k Zeilen) bei jedem Login einen Vollscan; die Kollation ist ohnehin case-insensitiv. EmailQuery vergleicht auf MySQL direkt über den Index, auf SQLite (Tests) weiter per LOWER. Neuer Index contacts_email_idx. F13: Firmen-Observer verwirft die öffentlichen Seiten-Caches bei Löschung, Wiederherstellung und is_active-Wechsel auf jedem Weg; die Newsroom-Seite prüft Sichtbarkeit vor dem Cache-Treffer; Kontakt- Änderungen im Pressefach flushen. Der Tombstone-Test wärmte den Cache vorher nicht und traf den Fehler nie. F12: Gemeldete URLs werden über Host, Ausgabe und Slug aufgelöst (ReportedUrlResolver); fremde Hosts zählen nicht, bei Doppelveröffentlichung gewinnt die Erstquelle. F11: per_page 1–100, status als Enum, language nur de/en, Kategorie muss aktiv sein und zum Ziel-Portal passen. Legacy-Aliasse behalten ihren Vertrag. F23: Rückgabewert von put() wird geprüft; Logo erst speichern, dann Datenbank, dann altes Logo löschen. Fehler enden als Feldfehler, nicht als verlorenes Logo. Block 4 — KI-Prüfung und Scheduler F07: Klassifikation und Score tragen die Prüfsumme des Inhalts bei Einplanung. Weicht sie beim Speichern ab, wird das Ergebnis verworfen (ki_audits.discarded_at) und ein neuer Lauf eingeplant. Der Scheduler veröffentlicht keine Meldung, deren jüngste Klassifikation nicht zum aktuellen Text passt, sondern plant eine Prüfung mit Routing ein. F18: Kontingent an den Kalendermonat gebunden (users.press_release_quota_period). Verbrauch aus einem früheren Monat zählt null, auch ohne Reset-Cron; der Reset ist idempotent und lässt den laufenden Monat in Ruhe. F20: Der Publikations-Scheduler liefert bei technischen Fehlern einen Fehler-Exit-Code, damit der Failure-Hook greift. F17: KI-Jobs mit $timeout = 120, retry_after 150; Runbook ergänzt. F19: Aggregation der Aufrufe unter einem Cache-Lock für alle Einstiege. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
238 lines
9.1 KiB
PHP
238 lines
9.1 KiB
PHP
<?php
|
|
|
|
use App\Enums\Portal;
|
|
use App\Enums\PressReleaseStatus;
|
|
use App\Models\Category;
|
|
use App\Models\Company;
|
|
use App\Models\PressRelease;
|
|
use App\Services\PressRelease\PressReleaseService;
|
|
use Illuminate\Support\Str;
|
|
use Tests\TestCase;
|
|
|
|
function makeNewsroom(string $name, Portal $portal = Portal::Businessportal24, array $attributes = []): Company
|
|
{
|
|
return Company::factory()
|
|
->state(['portal' => $portal->value])
|
|
->create(array_merge([
|
|
'name' => $name,
|
|
'slug' => Str::slug($name).'-'.fake()->unique()->numberBetween(1000, 9999),
|
|
'is_active' => true,
|
|
], $attributes));
|
|
}
|
|
|
|
function publishForCompany(Company $company, string $title, array $attributes = [], ?Category $category = null): PressRelease
|
|
{
|
|
$category ??= Category::factory()->create(['portal' => Portal::Both]);
|
|
|
|
return PressRelease::factory()
|
|
->published()
|
|
->forPortal(Portal::Businessportal24)
|
|
->for($category)
|
|
->for($company)
|
|
->create(array_merge([
|
|
'title' => $title,
|
|
'slug' => Str::slug($title).'-'.fake()->unique()->numberBetween(1000, 9999),
|
|
'language' => 'de',
|
|
'published_at' => now()->subDay(),
|
|
], $attributes));
|
|
}
|
|
|
|
test('the newsrooms index renders the editorial shell and lists active newsrooms', function () {
|
|
/** @var TestCase $this */
|
|
$company = makeNewsroom('Nordwind Energie AG');
|
|
publishForCompany($company, 'Nordwind nimmt neuen Windpark in Betrieb');
|
|
|
|
$this->get('https://businessportal24.test/de/newsrooms')
|
|
->assertSuccessful()
|
|
->assertSee('businessportal', false)
|
|
->assertSeeText('Newsrooms')
|
|
->assertSeeText('Nordwind Energie AG');
|
|
});
|
|
|
|
test('the newsrooms index filters by company name', function () {
|
|
/** @var TestCase $this */
|
|
$match = makeNewsroom('Solartec Industries GmbH');
|
|
publishForCompany($match, 'Solartec meldet Rekordquartal');
|
|
|
|
$other = makeNewsroom('Hafenlogistik Bremen AG');
|
|
publishForCompany($other, 'Hafenlogistik baut Terminal aus');
|
|
|
|
$this->get('https://businessportal24.test/de/newsrooms?q=Solartec')
|
|
->assertSuccessful()
|
|
->assertSeeText('Solartec Industries GmbH')
|
|
->assertDontSeeText('Hafenlogistik Bremen AG');
|
|
});
|
|
|
|
test('the newsrooms index excludes companies without published releases', function () {
|
|
/** @var TestCase $this */
|
|
makeNewsroom('Stilles Unternehmen AG');
|
|
|
|
$this->get('https://businessportal24.test/de/newsrooms')
|
|
->assertSuccessful()
|
|
->assertDontSeeText('Stilles Unternehmen AG');
|
|
});
|
|
|
|
test('the newsroom detail page renders company data and its releases', function () {
|
|
/** @var TestCase $this */
|
|
$company = makeNewsroom('Bavaria Maschinenbau AG', attributes: [
|
|
'boilerplate' => 'Bavaria Maschinenbau ist ein führender Anbieter von Industrielösungen.',
|
|
]);
|
|
publishForCompany($company, 'Bavaria präsentiert neue Produktionslinie');
|
|
|
|
$this->get('https://businessportal24.test/de/newsroom/'.$company->slug)
|
|
->assertSuccessful()
|
|
->assertSeeText('Bavaria Maschinenbau AG')
|
|
->assertSeeText('Bavaria präsentiert neue Produktionslinie')
|
|
->assertSeeText('Alle Meldungen');
|
|
});
|
|
|
|
test('the newsroom detail page links the company website with an absolute url', function () {
|
|
/** @var TestCase $this */
|
|
// Bestandsdaten ohne Schema („www.akgsoftware.de“) — als roher href
|
|
// würde daraus der relative Link /de/newsroom/www.akgsoftware.de.
|
|
$company = makeNewsroom('AKG Software Consulting GmbH', attributes: [
|
|
'website' => 'www.akgsoftware.de',
|
|
]);
|
|
publishForCompany($company, 'AKG veröffentlicht neue Version');
|
|
|
|
$this->get('https://businessportal24.test/de/newsroom/'.$company->slug)
|
|
->assertSuccessful()
|
|
->assertSee('href="https://www.akgsoftware.de"', false)
|
|
->assertDontSee('newsroom/www.akgsoftware.de', false);
|
|
|
|
// Hinterlegte URLs mit Schema bleiben unverändert.
|
|
expect(Company::factory()->make(['website' => 'https://akgsoftware.de'])->websiteUrl())
|
|
->toBe('https://akgsoftware.de')
|
|
->and(Company::factory()->make(['website' => null])->websiteUrl())->toBeNull();
|
|
});
|
|
|
|
test('the newsroom detail page returns 404 for an unknown slug', function () {
|
|
/** @var TestCase $this */
|
|
$this->get('https://businessportal24.test/de/newsroom/gibt-es-nicht')
|
|
->assertNotFound();
|
|
});
|
|
|
|
test('the newsroom detail page returns 404 when the company has no published releases', function () {
|
|
/** @var TestCase $this */
|
|
$company = makeNewsroom('Leeres Newsroom AG');
|
|
|
|
$this->get('https://businessportal24.test/de/newsroom/'.$company->slug)
|
|
->assertNotFound();
|
|
});
|
|
|
|
test('the newsroom page disappears with its last published release, cache included', function () {
|
|
/** @var TestCase $this */
|
|
// Roadmap 1b.1/1b.5: öffentlich nur mit veröffentlichter Meldung — und
|
|
// der Rückbau greift sofort, nicht erst nach Ablauf des Seiten-Caches.
|
|
$company = makeNewsroom('Einmalig AG');
|
|
$release = publishForCompany($company, 'Die einzige Meldung');
|
|
$url = 'https://businessportal24.test/de/newsroom/'.$company->slug;
|
|
|
|
$this->get($url)->assertOk();
|
|
$this->get('https://businessportal24.test/de/newsrooms')->assertSeeText('Einmalig AG');
|
|
|
|
app(PressReleaseService::class)->archive($release);
|
|
|
|
$this->get($url)->assertNotFound();
|
|
$this->get('https://businessportal24.test/de/newsrooms')->assertDontSeeText('Einmalig AG');
|
|
});
|
|
|
|
test('an admin status change away from published takes the newsroom page down', function () {
|
|
/** @var TestCase $this */
|
|
$company = makeNewsroom('Rückzug GmbH');
|
|
$release = publishForCompany($company, 'Zurückgezogen');
|
|
$url = 'https://businessportal24.test/de/newsroom/'.$company->slug;
|
|
|
|
$this->get($url)->assertOk();
|
|
|
|
app(PressReleaseService::class)->changeStatusFromAdmin($release, PressReleaseStatus::Draft);
|
|
|
|
$this->get($url)->assertNotFound();
|
|
});
|
|
|
|
test('deleting the last published release from admin takes the newsroom page down', function () {
|
|
/** @var TestCase $this */
|
|
$company = makeNewsroom('Gelöscht KG');
|
|
$release = publishForCompany($company, 'Wird entfernt');
|
|
$url = 'https://businessportal24.test/de/newsroom/'.$company->slug;
|
|
|
|
$this->get($url)->assertOk();
|
|
|
|
app(PressReleaseService::class)->deleteFromAdmin($release);
|
|
|
|
$this->get($url)->assertNotFound();
|
|
});
|
|
|
|
test('the newsrooms pages are available per edition and portal', function () {
|
|
/** @var TestCase $this */
|
|
$this->get('https://businessportal24.test/en/newsrooms')->assertSuccessful();
|
|
$this->get('https://presseecho.test/de/newsrooms')
|
|
->assertSuccessful()
|
|
->assertSee('presse', false);
|
|
});
|
|
|
|
test('legacy newsroom html urls redirect to the clean edition path', function () {
|
|
/** @var TestCase $this */
|
|
$this->get('https://businessportal24.test/de/newsrooms.html')
|
|
->assertRedirect('https://businessportal24.test/de/newsrooms');
|
|
|
|
$this->get('https://businessportal24.test/de/newsroom/acme.html')
|
|
->assertRedirect('https://businessportal24.test/de/newsroom/acme');
|
|
});
|
|
|
|
test('soft-deleting a company takes a cached newsroom page down immediately', function () {
|
|
/** @var TestCase $this */
|
|
$company = makeNewsroom('Aufgelöst GmbH');
|
|
publishForCompany($company, 'Letzte Meldung vor der Löschung');
|
|
$url = 'https://businessportal24.test/de/newsroom/'.$company->slug;
|
|
|
|
// Erst aufwärmen: ohne warmen Seiten-Cache prüft der Test nur den
|
|
// Neuaufbau, nicht das Verhalten beim Cache-Treffer (F13).
|
|
$this->get($url)->assertOk();
|
|
|
|
$company->delete();
|
|
|
|
$this->get($url)->assertNotFound();
|
|
});
|
|
|
|
test('deactivating a company takes a cached newsroom page down immediately', function () {
|
|
/** @var TestCase $this */
|
|
$company = makeNewsroom('Ruhend AG');
|
|
publishForCompany($company, 'Letzte Meldung vor der Deaktivierung');
|
|
$url = 'https://businessportal24.test/de/newsroom/'.$company->slug;
|
|
|
|
$this->get($url)->assertOk();
|
|
|
|
$company->update(['is_active' => false]);
|
|
|
|
$this->get($url)->assertNotFound();
|
|
});
|
|
|
|
test('a deactivated company beats a warm newsroom page cache even without a flush', function () {
|
|
/** @var TestCase $this */
|
|
$company = makeNewsroom('Still deaktiviert GmbH');
|
|
publishForCompany($company, 'Meldung im Cache');
|
|
$url = 'https://businessportal24.test/de/newsroom/'.$company->slug;
|
|
|
|
$this->get($url)->assertOk();
|
|
|
|
// Ohne Model-Events, also ohne Observer-Flush: So prüft der Test die
|
|
// Sichtbarkeitsprüfung VOR dem Cache-Treffer in NewsroomDetailPageData.
|
|
$company->forceFill(['is_active' => false])->saveQuietly();
|
|
|
|
$this->get($url)->assertNotFound();
|
|
});
|
|
|
|
test('a soft-deleted company beats a warm newsroom page cache even without a flush', function () {
|
|
/** @var TestCase $this */
|
|
$company = makeNewsroom('Still gelöscht GmbH');
|
|
publishForCompany($company, 'Meldung im Cache');
|
|
$url = 'https://businessportal24.test/de/newsroom/'.$company->slug;
|
|
|
|
$this->get($url)->assertOk();
|
|
|
|
// Query-Builder statt delete(): keine Events, kein Flush.
|
|
Company::query()->withoutGlobalScopes()->whereKey($company->id)->update(['deleted_at' => now()]);
|
|
|
|
$this->get($url)->assertNotFound();
|
|
});
|