seed(RolesAndPermissionsSeeder::class); // Rolle mit Admin-Zugang (editor), aber ohne press-releases:publish. Role::findByName('editor')->revokePermissionTo('press-releases:publish'); app(PermissionRegistrar::class)->forgetCachedPermissions(); $editor = User::factory()->create(['is_active' => true]); $editor->assignRole('editor'); $this->actingAs($editor); $pressRelease = PressRelease::factory()->create(['status' => PressReleaseStatus::Review->value]); LivewireVolt::test('admin.press-releases.edit', ['id' => $pressRelease->id]) ->set('targetStatus', PressReleaseStatus::Published->value) ->call('changeStatus') ->assertForbidden(); expect($pressRelease->fresh()->status)->toBe(PressReleaseStatus::Review); }); test('publishing without remaining quota shows a warning instead of a 500', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); // Kontingent greift erst mit dem Launch-Schalter. config()->set('billing.enforce_booking', true); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); // Autor mit erschöpftem Plan-Kontingent und ohne Einzelkauf. $author = User::factory()->create(['is_active' => true, 'press_release_quota_used_this_month' => 3]); $author->assignRole('customer'); $plan = Plan::factory()->create([ 'press_release_quota' => 3, 'stripe_price_id_monthly' => 'price_test_m_'.fake()->unique()->randomNumber(6), ]); subscribeUserToPlan($author, $plan); $company = Company::factory()->presseecho()->create(); $author->companies()->attach($company->id, ['role' => 'owner']); $pressRelease = PressRelease::factory()->create([ 'user_id' => $author->id, 'company_id' => $company->id, 'portal' => $company->portal->value, 'status' => PressReleaseStatus::Review->value, ]); $this->actingAs($admin); // Kein unbehandelter QuotaExceededException / kein 500: die Komponente fängt // ihn ab und die PM bleibt in Review. LivewireVolt::test('admin.press-releases.edit', ['id' => $pressRelease->id]) ->call('publish') ->assertHasNoErrors() ->assertOk(); expect($pressRelease->fresh()->status)->toBe(PressReleaseStatus::Review); }); test('published press releases are tombstoned instead of soft deleted', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); $replacementText = 'Diese Meldung wurde redaktionell entfernt.'; AdminPreset::factory()->create([ 'key' => AdminPreset::PRESS_RELEASE_DELETED_PUBLISHED_TEXT, 'area' => 'press_releases', 'type' => 'text', 'label' => 'Ersatztext', 'value' => $replacementText, ]); $pressRelease = PressRelease::factory() ->published() ->create(['text' => 'Alter veröffentlichter Inhalt.']); LivewireVolt::test('admin.press-releases.edit', ['id' => $pressRelease->id]) ->call('deletePressRelease') ->assertRedirect(route('admin.press-releases.index')); $pressRelease->refresh(); expect($pressRelease->deleted_at)->toBeNull() ->and($pressRelease->status)->toBe(PressReleaseStatus::Archived) ->and($pressRelease->text)->toBe($replacementText) ->and($pressRelease->no_export)->toBeTrue(); }); test('unpublished press releases are soft deleted by admin delete action', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); $pressRelease = PressRelease::factory()->create([ 'status' => PressReleaseStatus::Draft->value, ]); LivewireVolt::test('admin.press-releases.edit', ['id' => $pressRelease->id]) ->call('deletePressRelease') ->assertRedirect(route('admin.press-releases.index')); $this->assertSoftDeleted($pressRelease); }); test('edit page renders status selector and confirmation modal', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); $pressRelease = PressRelease::factory() ->inReview() ->create(); LivewireVolt::test('admin.press-releases.edit', ['id' => $pressRelease->id]) ->assertSee('Neuer Status') ->assertSee('Status wechseln') ->assertSee('Status wirklich wechseln?') ->assertSee('Pressemitteilung löschen?'); }); test('admin can change archived press release back to another status', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); $pressRelease = PressRelease::factory()->create([ 'status' => PressReleaseStatus::Archived->value, ]); LivewireVolt::test('admin.press-releases.edit', ['id' => $pressRelease->id]) ->set('targetStatus', PressReleaseStatus::Draft->value) ->call('changeStatus') ->assertHasNoErrors(); $pressRelease->refresh(); expect($pressRelease->status)->toBe(PressReleaseStatus::Draft); }); test('press release index renders confirmation modals for status actions', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); PressRelease::factory() ->inReview() ->create(['title' => 'PM in Prüfung']); PressRelease::factory() ->published() ->create(['title' => 'Veröffentlichte PM']); LivewireVolt::test('admin.press-releases.index') ->assertSee('Pressemitteilung veröffentlichen?') ->assertSee('Pressemitteilung ablehnen?') ->assertSee('Pressemitteilung archivieren?'); }); test('ai check modal shows classification details from the latest audit', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); $pressRelease = PressRelease::factory() ->inReview() ->create(['classification' => PressReleaseClassification::Yellow->value]); KiAudit::factory() ->classification(PressReleaseClassification::Yellow) ->create([ 'press_release_id' => $pressRelease->id, 'reason' => 'Werbliche Sprache im Titel gefunden.', 'provider' => 'anthropic', ]); LivewireVolt::test('admin.press-releases.index') ->call('openAiCheck', $pressRelease->id) ->assertSet('aiCheckId', $pressRelease->id) ->assertSee('Werbliche Sprache im Titel gefunden.') ->assertSee('anthropic') ->assertSee('Erneut prüfen'); }); test('ai check modal shows empty state and start button for unchecked press releases', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); $pressRelease = PressRelease::factory() ->inReview() ->create(['classification' => null]); LivewireVolt::test('admin.press-releases.index') ->call('openAiCheck', $pressRelease->id) ->assertSee('Noch nicht geprüft') ->assertSee('Noch nicht bewertet') ->assertSee('Prüfung starten'); }); test('ai check modal shows the content score breakdown from the audit raw response', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); $pressRelease = PressRelease::factory() ->inReview() ->create([ 'classification' => PressReleaseClassification::Green->value, 'content_score' => 62, 'content_tier' => PressReleaseContentTier::Geprueft->value, ]); KiAudit::factory() ->contentScore(62) ->create([ 'press_release_id' => $pressRelease->id, 'provider' => 'openai', 'raw_response' => [ 'breakdown' => [ 'pressestil' => 12, 'struktur' => 8, 'bild' => 0, ], ], ]); LivewireVolt::test('admin.press-releases.index') ->call('openAiCheck', $pressRelease->id) ->assertSee('Pressestil') ->assertSee('12 / 25') ->assertSee('Struktur') ->assertSee('8 / 15') ->assertSee('Bildmaterial') ->assertSee('0 / 10'); }); test('ai check modal explains missing reason for green classifications', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); $pressRelease = PressRelease::factory() ->inReview() ->create(['classification' => PressReleaseClassification::Green->value]); KiAudit::factory() ->classification(PressReleaseClassification::Green) ->create([ 'press_release_id' => $pressRelease->id, 'reason' => null, ]); LivewireVolt::test('admin.press-releases.index') ->call('openAiCheck', $pressRelease->id) ->assertSee('bei Grün liefert die KI keine Begründung'); }); test('runAiCheck from modal dispatches jobs without routing and marks the run as pending', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); Queue::fake(); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); $pressRelease = PressRelease::factory() ->inReview() ->create(['classification' => null]); LivewireVolt::test('admin.press-releases.index') ->call('openAiCheck', $pressRelease->id) ->assertSet('aiCheckStartedAt', null) ->call('runAiCheck') ->assertSee('Prüfung läuft'); Queue::assertPushed(ClassifyPressRelease::class, fn (ClassifyPressRelease $job): bool => $job->pressReleaseId === $pressRelease->id && $job->route === false); Queue::assertPushed(ScorePressRelease::class, fn (ScorePressRelease $job): bool => $job->pressReleaseId === $pressRelease->id); }); test('show page renders confirmation modals for status actions', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); $reviewPressRelease = PressRelease::factory() ->inReview() ->create(); LivewireVolt::test('admin.press-releases.show', ['id' => $reviewPressRelease->id]) ->assertSee('Pressemitteilung veröffentlichen?') ->assertSee('Pressemitteilung ablehnen?'); $publishedPressRelease = PressRelease::factory() ->published() ->create(); LivewireVolt::test('admin.press-releases.show', ['id' => $publishedPressRelease->id]) ->assertSee('Pressemitteilung archivieren?'); }); /* * K-06 (Umsetzungsaudit 29.09.2026): Die Detailseite verlangt für jede * statusändernde Aktion dieselbe Berechtigung wie Liste und Bearbeiten. */ test('show page actions are forbidden without the publish permission', function (string $action, array $set) { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); Role::findByName('editor')->revokePermissionTo('press-releases:publish'); app(PermissionRegistrar::class)->forgetCachedPermissions(); $editor = User::factory()->create(['is_active' => true]); $editor->assignRole('editor'); $this->actingAs($editor); $pressRelease = PressRelease::factory()->create(['status' => PressReleaseStatus::Review->value]); $component = LivewireVolt::test('admin.press-releases.show', ['id' => $pressRelease->id]); foreach ($set as $property => $value) { $component->set($property, $value); } $component->call($action)->assertForbidden(); expect($pressRelease->fresh()->status)->toBe(PressReleaseStatus::Review); })->with([ 'publish' => ['publish', []], 'reject' => ['reject', ['rejectReason' => 'Bitte überarbeiten.']], 'archive' => ['archive', []], 'correctImage' => ['correctImage', ['correctionReason' => 'Falscher Nachweis.']], 'approvePrecheck' => ['approvePrecheck', ['precheckNote' => 'Geprüft und ok.']], 'rejectPrecheck' => ['rejectPrecheck', ['precheckNote' => 'Geprüft, nicht ok.']], 'toggleWithheld' => ['toggleWithheld', ['correctionReason' => 'Beschwerde eingegangen.']], ]); test('the manual ai check from the list needs the publish permission because a red result can reject', function () { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); Queue::fake(); Role::findByName('editor')->revokePermissionTo('press-releases:publish'); app(PermissionRegistrar::class)->forgetCachedPermissions(); $editor = User::factory()->create(['is_active' => true]); $editor->assignRole('editor'); $this->actingAs($editor); $pressRelease = PressRelease::factory()->inReview()->create(); LivewireVolt::test('admin.press-releases.index') ->call('runAiCheck', $pressRelease->id) ->assertForbidden(); Queue::assertNotPushed(ClassifyPressRelease::class); }); test('publishing from the show page reports a blocked publish instead of a 500', function (Closure $exception) { /** @var TestCase $this */ $this->seed(RolesAndPermissionsSeeder::class); $admin = User::factory()->create(['is_active' => true]); $admin->assignRole('admin'); $this->actingAs($admin); $pressRelease = PressRelease::factory()->inReview()->create(); $this->mock(PressReleaseService::class)->shouldReceive('publish')->twice()->andThrow($exception()); LivewireVolt::test('admin.press-releases.show', ['id' => $pressRelease->id]) ->call('publish') ->assertOk(); LivewireVolt::test('admin.press-releases.index') ->call('publish', $pressRelease->id) ->assertOk(); })->with([ 'quota' => [fn () => new QuotaExceededException], 'status' => [fn () => new LogicException('Status passt nicht.')], 'image rights' => [fn () => new ImageRightsConfirmationRequiredException], ]);