presseportale/tests/Feature/PressReleaseWorkflowTest.php
Kevin Adametz f6934f54e1 mail: Anrede an den tatsächlichen Empfänger binden
Bei Einreichungen über die API geht die Veröffentlichungs- oder
Ablehnungsmail nicht an das API-Konto, sondern an den Pressekontakt der
Meldung oder ersatzweise an die Firmen-Adresse. Die Anrede stammte aber
weiterhin vom Konto — der Empfänger wurde also mit dem Namen des
Einreichers begrüßt, bei connektar mit dem der Agentur.

Adresse und Name werden jetzt gemeinsam bestimmt. Ohne hinterlegten Namen
bleibt er leer und das Template grüßt neutral, statt einen falschen zu raten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 11:07:23 +00:00

298 lines
9.7 KiB
PHP

<?php
use App\Enums\PressReleaseStatus;
use App\Mail\PressReleasePublished;
use App\Mail\PressReleaseRejected;
use App\Models\PressRelease;
use App\Models\PressReleaseStatusLog;
use App\Models\User;
use App\Services\Admin\AdminPerformanceCache;
use App\Services\PressRelease\PressReleaseService;
use Database\Seeders\RolesAndPermissionsSeeder;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Queue;
use Livewire\Volt\Volt as LivewireVolt;
use Tests\TestCase;
beforeEach(function (): void {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
// Klassifikations-Job nicht inline ausführen (sync-Queue im Test), damit
// submitForReview hier nur den Übergang nach „review" prüft. Das KI-Routing
// (Grün→publish etc.) wird separat in PressReleaseClassificationJobTest getestet.
Queue::fake();
});
test('submit for review logs a status change with customer source', function () {
/** @var TestCase $this */
$customer = User::factory()->create(['is_active' => true]);
$customer->assignRole('customer');
$pr = PressRelease::factory()->create([
'user_id' => $customer->id,
'status' => PressReleaseStatus::Draft->value,
]);
$this->actingAs($customer);
app(PressReleaseService::class)->submitForReview($pr);
expect($pr->fresh()->status)->toBe(PressReleaseStatus::Review);
$log = PressReleaseStatusLog::query()->latest('id')->firstOrFail();
expect($log->press_release_id)->toBe($pr->id);
expect($log->from_status)->toBe(PressReleaseStatus::Draft);
expect($log->to_status)->toBe(PressReleaseStatus::Review);
expect($log->source)->toBe('customer');
expect($log->changed_by_user_id)->toBe($customer->id);
});
test('admin can reject a review press release with a reason', function () {
/** @var TestCase $this */
Mail::fake();
$admin = User::factory()->create(['is_active' => true]);
$admin->assignRole('admin');
$author = User::factory()->create(['is_active' => true]);
$author->assignRole('customer');
$pr = PressRelease::factory()->create([
'user_id' => $author->id,
'status' => PressReleaseStatus::Review->value,
]);
$this->actingAs($admin);
LivewireVolt::test('admin.press-releases.show', ['id' => $pr->id])
->set('rejectReason', 'Bitte überarbeiten Sie den werblichen Sprachstil.')
->call('reject')
->assertHasNoErrors();
$pr->refresh();
expect($pr->status)->toBe(PressReleaseStatus::Rejected);
$log = PressReleaseStatusLog::query()
->where('press_release_id', $pr->id)
->latest('id')
->firstOrFail();
expect($log->to_status)->toBe(PressReleaseStatus::Rejected);
expect($log->reason)->toBe('Bitte überarbeiten Sie den werblichen Sprachstil.');
expect($log->changed_by_user_id)->toBe($admin->id);
Mail::assertQueued(PressReleaseRejected::class, function (PressReleaseRejected $mail) use ($author) {
return $mail->user->is($author)
&& $mail->reason === 'Bitte überarbeiten Sie den werblichen Sprachstil.';
});
});
test('admin reject without reason fails validation', function () {
/** @var TestCase $this */
$admin = User::factory()->create(['is_active' => true]);
$admin->assignRole('admin');
$pr = PressRelease::factory()->create([
'status' => PressReleaseStatus::Review->value,
]);
$this->actingAs($admin);
LivewireVolt::test('admin.press-releases.show', ['id' => $pr->id])
->set('rejectReason', '')
->call('reject')
->assertHasErrors(['rejectReason']);
expect($pr->fresh()->status)->toBe(PressReleaseStatus::Review);
});
test('admin publish notifies customer with me edit url', function () {
/** @var TestCase $this */
Mail::fake();
$admin = User::factory()->create(['is_active' => true]);
$admin->assignRole('admin');
$author = User::factory()->create(['is_active' => true]);
$author->assignRole('customer');
$pr = PressRelease::factory()->create([
'user_id' => $author->id,
'status' => PressReleaseStatus::Review->value,
]);
$this->actingAs($admin);
app(PressReleaseService::class)->publish($pr);
Mail::assertQueued(PressReleasePublished::class, function (PressReleasePublished $mail) use ($pr) {
return str_contains($mail->showUrl, '/admin/me/press-releases/'.$pr->id);
});
});
test('rejected mail uses customer edit url for customer recipients', function () {
/** @var TestCase $this */
Mail::fake();
$admin = User::factory()->create(['is_active' => true]);
$admin->assignRole('admin');
$author = User::factory()->create(['is_active' => true]);
$author->assignRole('customer');
$pr = PressRelease::factory()->create([
'user_id' => $author->id,
'status' => PressReleaseStatus::Review->value,
]);
$this->actingAs($admin);
app(PressReleaseService::class)->reject($pr, 'Test-Grund');
Mail::assertQueued(PressReleaseRejected::class, function (PressReleaseRejected $mail) use ($pr) {
return str_contains($mail->editUrl, '/admin/me/press-releases/'.$pr->id.'/edit');
});
});
test('rejected mail renders multi-line reason with line breaks', function () {
/** @var TestCase $this */
$author = User::factory()->create(['is_active' => true]);
$author->assignRole('customer');
$pr = PressRelease::factory()->create([
'user_id' => $author->id,
'status' => PressReleaseStatus::Rejected->value,
]);
$html = (new PressReleaseRejected($author, $pr, "Erster Punkt fehlt.\nZweiter Punkt fehlt.\n\nBitte überarbeiten."))->render();
expect($html)
->toContain('Erster Punkt fehlt.<br')
->toContain('Zweiter Punkt fehlt.<br');
});
test('admin can resubmit cycle creates two audit log entries', function () {
/** @var TestCase $this */
$author = User::factory()->create(['is_active' => true]);
$author->assignRole('customer');
$admin = User::factory()->create(['is_active' => true]);
$admin->assignRole('admin');
$pr = PressRelease::factory()->create([
'user_id' => $author->id,
'status' => PressReleaseStatus::Draft->value,
]);
$this->actingAs($author);
app(PressReleaseService::class)->submitForReview($pr);
$this->actingAs($admin);
app(PressReleaseService::class)->reject($pr->fresh(), 'Bitte korrigieren');
$this->actingAs($author);
app(PressReleaseService::class)->submitForReview($pr->fresh());
$logs = PressReleaseStatusLog::query()
->where('press_release_id', $pr->id)
->orderBy('id')
->get();
expect($logs)->toHaveCount(3);
expect($logs[0]->to_status)->toBe(PressReleaseStatus::Review);
expect($logs[1]->to_status)->toBe(PressReleaseStatus::Rejected);
expect($logs[2]->to_status)->toBe(PressReleaseStatus::Review);
});
test('customer show page surfaces latest reject reason prominently', function () {
/** @var TestCase $this */
$customer = User::factory()->create(['is_active' => true]);
$customer->assignRole('customer');
$pr = PressRelease::factory()->create([
'user_id' => $customer->id,
'status' => PressReleaseStatus::Rejected->value,
]);
PressReleaseStatusLog::query()->create([
'press_release_id' => $pr->id,
'changed_by_user_id' => null,
'from_status' => PressReleaseStatus::Review->value,
'to_status' => PressReleaseStatus::Rejected->value,
'reason' => 'Werbliche Sprache wurde markiert.',
'source' => 'admin',
'created_at' => now(),
]);
$this->actingAs($customer);
LivewireVolt::test('customer.press-releases.show', ['id' => $pr->id])
->assertSee('Werbliche Sprache wurde markiert.')
->assertSee('Erneut einreichen');
});
test('admin index publish goes through service and creates audit log', function () {
/** @var TestCase $this */
Mail::fake();
$admin = User::factory()->create(['is_active' => true]);
$admin->assignRole('admin');
$pr = PressRelease::factory()->create([
'status' => PressReleaseStatus::Review->value,
]);
$this->actingAs($admin);
LivewireVolt::test('admin.press-releases.index')
->call('publish', $pr->id)
->assertHasNoErrors();
expect($pr->fresh()->status)->toBe(PressReleaseStatus::Published);
$log = PressReleaseStatusLog::query()
->where('press_release_id', $pr->id)
->where('to_status', PressReleaseStatus::Published->value)
->firstOrFail();
expect($log->changed_by_user_id)->toBe($admin->id);
});
test('admin index reject from list creates audit log with default reason', function () {
/** @var TestCase $this */
Mail::fake();
$admin = User::factory()->create(['is_active' => true]);
$admin->assignRole('admin');
$pr = PressRelease::factory()->create([
'status' => PressReleaseStatus::Review->value,
]);
$this->actingAs($admin);
LivewireVolt::test('admin.press-releases.index')
->call('reject', $pr->id);
expect($pr->fresh()->status)->toBe(PressReleaseStatus::Rejected);
$log = PressReleaseStatusLog::query()
->where('press_release_id', $pr->id)
->latest('id')
->firstOrFail();
expect($log->reason)->not->toBeNull();
});
test('review counter helper returns correct number', function () {
/** @var TestCase $this */
PressRelease::factory()->count(3)->create(['status' => PressReleaseStatus::Review->value]);
PressRelease::factory()->count(2)->create(['status' => PressReleaseStatus::Draft->value]);
PressRelease::factory()->count(1)->create(['status' => PressReleaseStatus::Published->value]);
$count = app(AdminPerformanceCache::class)->pressReleaseReviewCount();
expect($count)->toBe(3);
});