- 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>
112 lines
4.8 KiB
PHP
112 lines
4.8 KiB
PHP
<?php
|
|
|
|
use App\Enums\Portal;
|
|
use App\Enums\PressReleaseEventType;
|
|
use App\Models\Category;
|
|
use App\Models\Company;
|
|
use App\Models\PressRelease;
|
|
use App\Models\PressReleaseEvent;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Umsetzungsaudit 29.09.2026, R-02/K-03 und R-03: `updated_at` taugt nicht
|
|
* als Änderungsdatum (Aufrufaggregation, Merge). Sitemap-lastmod, JSON-LD
|
|
* dateModified und article:modified_time folgen `content_changed_at`, die
|
|
* Sitemap führt englische Meldungen unter /en/.
|
|
*/
|
|
function lastmodRelease(array $attributes = []): PressRelease
|
|
{
|
|
$category = Category::factory()->create(['portal' => Portal::Presseecho, 'is_active' => true]);
|
|
$company = Company::factory()->presseecho()->create(['is_active' => true]);
|
|
|
|
return PressRelease::factory()
|
|
->published()
|
|
->forPortal(Portal::Presseecho)
|
|
->for($category)
|
|
->for($company)
|
|
->create(array_merge(['slug' => 'lastmod-pm', 'published_at' => Carbon::parse('2026-01-10 10:00:00')], $attributes));
|
|
}
|
|
|
|
test('view aggregation leaves updated_at untouched', function () {
|
|
/** @var TestCase $this */
|
|
$release = lastmodRelease(['hits' => 0]);
|
|
$before = PressRelease::withoutGlobalScopes()->find($release->id)->updated_at;
|
|
|
|
PressReleaseEvent::factory()->count(3)->create([
|
|
'press_release_id' => $release->id,
|
|
'type' => PressReleaseEventType::View,
|
|
]);
|
|
|
|
$this->travel(2)->days();
|
|
$this->artisan('press-releases:aggregate-views')->assertSuccessful();
|
|
|
|
$fresh = PressRelease::withoutGlobalScopes()->find($release->id);
|
|
expect($fresh->hits)->toBe(3)
|
|
->and($fresh->updated_at->equalTo($before))->toBeTrue();
|
|
});
|
|
|
|
test('a text change sets content_changed_at, other saves do not', function () {
|
|
$release = lastmodRelease();
|
|
expect($release->fresh()->content_changed_at)->toBeNull();
|
|
|
|
$this->travel(1)->days();
|
|
$release->update(['keywords' => 'nur, schlagworte']);
|
|
expect($release->fresh()->content_changed_at)->toBeNull();
|
|
|
|
$this->travel(1)->days();
|
|
$release->update(['text' => '<p>Neuer Text</p>']);
|
|
expect($release->fresh()->content_changed_at?->toDateTimeString())->toBe(now()->toDateTimeString());
|
|
});
|
|
|
|
test('sitemap lastmod follows content_changed_at and ignores updated_at', function () {
|
|
/** @var TestCase $this */
|
|
$release = lastmodRelease();
|
|
|
|
// Aufrufaggregation / Merge alter Art: updated_at springt, Inhalt nicht.
|
|
PressRelease::withoutGlobalScopes()->whereKey($release->id)->toBase()->update(['updated_at' => '2026-09-20 12:00:00']);
|
|
|
|
Cache::flush();
|
|
$xml = $this->get('https://presseecho.test/sitemap.xml')->assertSuccessful()->getContent();
|
|
expect($xml)->toContain('<lastmod>'.Carbon::parse('2026-01-10 10:00:00')->toAtomString().'</lastmod>')
|
|
->not->toContain('2026-09-20');
|
|
|
|
PressRelease::withoutGlobalScopes()->whereKey($release->id)->toBase()->update(['content_changed_at' => '2026-03-05 08:00:00']);
|
|
|
|
Cache::flush();
|
|
$xml = $this->get('https://presseecho.test/sitemap.xml')->assertSuccessful()->getContent();
|
|
expect($xml)->toContain('<lastmod>'.Carbon::parse('2026-03-05 08:00:00')->toAtomString().'</lastmod>');
|
|
});
|
|
|
|
test('the detail page reports content_changed_at as modification date', function () {
|
|
/** @var TestCase $this */
|
|
$release = lastmodRelease();
|
|
PressRelease::withoutGlobalScopes()->whereKey($release->id)->toBase()->update(['updated_at' => '2026-09-20 12:00:00']);
|
|
|
|
$html = $this->get('https://presseecho.test/de/press-release/lastmod-pm')->assertSuccessful()->getContent();
|
|
expect($html)->not->toContain('article:modified_time')
|
|
->not->toContain('2026-09-20T12:00:00');
|
|
|
|
PressRelease::withoutGlobalScopes()->whereKey($release->id)->toBase()->update(['content_changed_at' => '2026-03-05 08:00:00']);
|
|
Cache::flush();
|
|
|
|
$html = $this->get('https://presseecho.test/de/press-release/lastmod-pm')->assertSuccessful()->getContent();
|
|
$modified = Carbon::parse('2026-03-05 08:00:00')->toIso8601String();
|
|
expect($html)->toContain('<meta property="article:modified_time" content="'.$modified.'">')
|
|
->toContain('"dateModified":"'.str_replace('/', '\/', $modified).'"');
|
|
});
|
|
|
|
test('the sitemap lists english releases under the english edition', function () {
|
|
/** @var TestCase $this */
|
|
lastmodRelease(['slug' => 'english-release', 'language' => 'en']);
|
|
lastmodRelease(['slug' => 'deutsche-meldung', 'language' => 'de']);
|
|
|
|
$xml = $this->get('https://presseecho.test/sitemap.xml')->assertSuccessful()->getContent();
|
|
|
|
expect($xml)->toContain('<loc>https://presseecho.test/en/press-release/english-release</loc>')
|
|
->toContain('<loc>https://presseecho.test/de/press-release/deutsche-meldung</loc>')
|
|
->not->toContain('/de/press-release/english-release');
|
|
|
|
$this->get('https://presseecho.test/en/press-release/english-release')->assertSuccessful();
|
|
});
|