presseportale/tests/Feature/ChangelogTest.php
Kevin Adametz e9cd572aa4 web(hub): Changelog im Header vor der FAQ und im Footer unter Plattform
Changelog: Verbessert
Public: Der Changelog ist auf pressekonto.com über die Navigation und den Footer erreichbar.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:49:52 +00:00

204 lines
9.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
use App\Enums\ChangelogCategory;
use App\Enums\ChangelogStatus;
use App\Models\ChangelogEntry;
use App\Models\User;
use App\Services\Changelog\ChangelogCollector;
use App\Services\Changelog\ChangelogTimeline;
use Database\Seeders\RolesAndPermissionsSeeder;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Process;
use Livewire\Livewire;
use Tests\TestCase;
/**
* Changelog (Decision-Update Changelog, Stufen 1–3): Entwürfe aus den
* Commit-Zeilen beim Deploy, Freigabe im Admin, öffentliche Seite mit RSS.
*/
/**
* Kleines Git-Repository mit einem Tag am Anfang und einem am Ende.
*
* @param list<string> $messages
*/
function changelogRepository(array $messages): string
{
$path = sys_get_temp_dir().'/changelog-test-'.uniqid();
File::ensureDirectoryExists($path);
$git = fn (array $command) => Process::path($path)->run(['git', '-c', 'user.name=Test', '-c', 'user.email=test@example.org', ...$command])->throw();
$git(['init', '--quiet']);
$git(['commit', '--allow-empty', '--quiet', '-m', 'start']);
$git(['tag', 'v0.1.0']);
foreach ($messages as $message) {
$git(['commit', '--allow-empty', '--quiet', '-m', $message]);
}
$git(['tag', 'v0.2.0']);
return $path;
}
// ---------------------------------------------------------- Stufe 1 und 2
test('the commit lines are read from anywhere in the message, not only from the trailer block', function () {
/** @var TestCase $this */
$parsed = ChangelogCollector::parse("web: Kasten\n\nText.\n\nChangelog: Verbessert\nPublic: Auf jeder Newsroom-Seite\nführt ein Kasten zum Zugang.\n\nCo-Authored-By: X <x@example.org>");
expect($parsed['category'])->toBe(ChangelogCategory::Improved)
->and($parsed['public'])->toBe('Auf jeder Newsroom-Seite führt ein Kasten zum Zugang.')
->and(ChangelogCollector::parse("dev: intern\n\nNur Doku."))->toBeNull()
->and(ChangelogCollector::parse("x: y\n\nChangelog: Irgendwas"))->toBeNull()
->and(ChangelogCollector::suggestionFrom('web(newsroom): Newsroom-Seite mit Kasten'))->toBe('Newsroom-Seite mit Kasten.');
});
test('a deploy collects drafts: internal commits stay out, security becomes one line, missing public sentences are marked', function () {
/** @var TestCase $this */
$repository = changelogRepository([
"web: Newsroom-Kasten\n\nChangelog: Neu\nPublic: Jede Newsroom-Seite hat einen Kasten zum Verwalten.",
'dev: Roadmap gepflegt',
"web: Firmenseite verlinkt den Newsroom\n\nChangelog: Verbessert",
"fix(auth): Session nach Passwortwechsel beenden\n\nChangelog: Behoben\nPublic: Nach dem Passwortwechsel ist man abgemeldet.",
"security: Header gesetzt\n\nChangelog: Sicherheit",
]);
$collector = new ChangelogCollector($repository);
$result = $collector->collect('v0.1.0', 'v0.2.0');
expect($result['created'])->toBe(3)
->and($result['internal'])->toBe(1);
$entries = ChangelogEntry::query()->orderBy('id')->get();
expect($entries->pluck('status')->unique()->all())->toBe([ChangelogStatus::Draft])
->and($entries[0]->text)->toBe('Jede Newsroom-Seite hat einen Kasten zum Verwalten.')
->and($entries[1]->text)->toBe('Firmenseite verlinkt den Newsroom.')
->and($entries[1]->review_note)->toContain('Kein Public-Satz')
// „Session“ im Titel, Rubrik Behoben → Sicherheit, ohne Beschreibung.
->and($entries->where('category', ChangelogCategory::Security))->toHaveCount(1)
->and($entries[2]->text)->toBe(ChangelogCategory::SECURITY_LINE)
->and($entries[2]->release_tag)->toBe('v0.2.0');
// Ein zweiter Lauf (Deploy wiederholt) legt nichts doppelt an.
expect($collector->collect('v0.1.0', 'v0.2.0')['created'])->toBe(0)
->and(ChangelogEntry::query()->count())->toBe(3);
File::deleteDirectory($repository);
});
test('an unknown tag fails loudly instead of collecting nothing', function () {
/** @var TestCase $this */
$repository = changelogRepository([]);
expect(fn () => (new ChangelogCollector($repository))->collect('v9.9.9', 'v0.2.0'))->toThrow(RuntimeException::class);
File::deleteDirectory($repository);
});
test('the backfill adds the entries since the relaunch as drafts, once', function () {
/** @var TestCase $this */
$this->artisan('changelog:backfill')->assertSuccessful();
$count = ChangelogEntry::query()->count();
$this->artisan('changelog:backfill')->assertSuccessful();
expect($count)->toBeGreaterThan(10)
->and(ChangelogEntry::query()->count())->toBe($count)
->and(ChangelogEntry::query()->where('status', '!=', ChangelogStatus::Draft->value)->exists())->toBeFalse();
});
// -------------------------------------------------------- Öffentliche Seite
test('the public changelog shows only published entries and security once per day', function () {
/** @var TestCase $this */
ChangelogEntry::factory()->published()->create(['released_on' => '2026-10-13', 'text' => 'Gebuchte Platzierungen sind gekennzeichnet.']);
ChangelogEntry::factory()->create(['released_on' => '2026-10-13', 'text' => 'Noch nicht freigegeben.']);
ChangelogEntry::factory()->published()->category(ChangelogCategory::Security)->count(2)->create(['released_on' => '2026-10-08', 'text' => 'Details zur Lücke']);
$response = $this->get('https://pressekonto.test/changelog')
->assertOk()
->assertSeeText('Was ist neu')
->assertSeeText('Gebuchte Platzierungen sind gekennzeichnet.')
->assertDontSeeText('Noch nicht freigegeben.')
->assertDontSeeText('Details zur Lücke')
->assertSee('/changelog/feed', false);
expect(substr_count($response->getContent(), ChangelogCategory::SECURITY_LINE))->toBe(1);
});
test('the changelog belongs to the hub and the feed is rss', function () {
/** @var TestCase $this */
ChangelogEntry::factory()->published()->create(['text' => 'Rechnungen kommen per E-Mail.']);
$this->get('https://presseecho.test/changelog')->assertRedirect('https://pressekonto.test/changelog');
$this->get('https://pressekonto.test/changelog/feed')
->assertOk()
->assertHeader('Content-Type', 'application/rss+xml; charset=UTF-8')
->assertSee('<rss version="2.0"', false)
->assertSee('Rechnungen kommen per E-Mail.', false);
});
test('the hub links the changelog in the header before the faq and in the footer under platform', function () {
/** @var TestCase $this */
$html = $this->get('https://pressekonto.test/')->assertOk()->getContent();
expect(substr_count($html, 'href="'.route('changelog').'"'))->toBeGreaterThanOrEqual(3)
->and(strpos($html, '>Changelog<'))->toBeLessThan(strpos($html, '#faq'));
});
test('the new hint shows only with a recent published entry', function () {
/** @var TestCase $this */
$timeline = app(ChangelogTimeline::class);
ChangelogEntry::factory()->published()->create(['released_on' => now()->subDays(30)->toDateString()]);
expect($timeline->hasRecent())->toBeFalse();
ChangelogEntry::factory()->published()->create(['released_on' => now()->subDays(2)->toDateString()]);
expect($timeline->hasRecent())->toBeTrue();
});
// ------------------------------------------------------------- Admin
test('admins review, edit and publish drafts', function () {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
$admin = User::factory()->create(['is_active' => true]);
$admin->assignRole('admin');
$draft = ChangelogEntry::factory()->create(['released_on' => '2026-10-14', 'text' => 'Erster Entwurf des Satzes.']);
$other = ChangelogEntry::factory()->create(['released_on' => '2026-10-14']);
$this->actingAs($admin)->get(route('admin.changelog.index'))->assertOk()->assertSeeText('Erster Entwurf des Satzes.');
Livewire::test('admin.changelog.index')
->call('edit', $draft->id)
->set('editText', 'Kurz')
->call('saveEdit')
->assertHasErrors('editText')
->set('editText', 'Die Firmenseite verlinkt den Newsroom.')
->set('editCategory', ChangelogCategory::New->value)
->call('saveEdit')
->assertHasNoErrors()
->call('publish', $draft->id)
->call('discard', $other->id);
$draft->refresh();
expect($draft->text)->toBe('Die Firmenseite verlinkt den Newsroom.')
->and($draft->category)->toBe(ChangelogCategory::New)
->and($draft->status)->toBe(ChangelogStatus::Published)
->and($draft->published_at)->not->toBeNull()
->and($draft->reviewed_by_user_id)->toBe($admin->id)
->and($other->fresh()->status)->toBe(ChangelogStatus::Discarded);
Livewire::test('admin.changelog.index')->call('restore', $other->id)->call('publishDay', '2026-10-14');
expect($other->fresh()->status)->toBe(ChangelogStatus::Published);
});
test('editors without settings permission cannot open the changelog admin', function () {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
$editor = User::factory()->create(['is_active' => true]);
$editor->assignRole('editor');
$this->actingAs($editor)->get(route('admin.changelog.index'))->assertForbidden();
});