admin: Boost vorzeitig beenden, Berechtigungsliste audit:admins
Admin ▸ Pressemitteilung zeigt laufende Boosts; „Boost beenden“ verlangt einen Grund und billing:manage, beendet laufende und angeschlossene Zeiträume, verwirft die Seiten-Caches und protokolliert „Boost beendet“. Credits werden nicht zurückgebucht. audit:admins listet alle Konten mit Rechten über den Kundenbereich hinaus (Super-Admin, Rollen außer customer/api-only, direkte Permissions), auch gesperrte und gelöschte, als Markdown mit 2FA, Passkeys, Tokens und letztem Sitzungswiderruf. E-Mail standardmäßig nur mit Domain (Nachweis BN-02). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2150d4b7ae
commit
fce7baea2b
6 changed files with 419 additions and 0 deletions
91
tests/Feature/Admin/EndBoostTest.php
Normal file
91
tests/Feature/Admin/EndBoostTest.php
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ActivityType;
|
||||
use App\Enums\PressReleaseClassification;
|
||||
use App\Models\ActivityEvent;
|
||||
use App\Models\Boost;
|
||||
use App\Models\PressRelease;
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Livewire\Livewire;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Gebuchte Platzierung vorzeitig beenden (Rechtspaket 10/2026, U10): der
|
||||
* Admin beendet laufende und angeschlossene Boosts mit Pflichtgrund; die
|
||||
* Buchung bleibt als Nachweis stehen, der Vorgang landet im Verlauf.
|
||||
*/
|
||||
beforeEach(function (): void {
|
||||
/** @var TestCase $this */
|
||||
$this->seed(RolesAndPermissionsSeeder::class);
|
||||
|
||||
$this->pressRelease = PressRelease::factory()->published()->create([
|
||||
'classification' => PressReleaseClassification::Green->value,
|
||||
]);
|
||||
});
|
||||
|
||||
test('an admin ends running and queued boosts with a reason', function () {
|
||||
/** @var TestCase $this */
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole('admin');
|
||||
|
||||
$running = Boost::factory()->for($this->pressRelease)->create();
|
||||
$queued = Boost::factory()->for($this->pressRelease)->create([
|
||||
'starts_at' => now()->addDays(7),
|
||||
'ends_at' => now()->addDays(14),
|
||||
]);
|
||||
$expired = Boost::factory()->for($this->pressRelease)->expired()->create();
|
||||
$expiredEnd = $expired->ends_at;
|
||||
|
||||
$this->actingAs($admin);
|
||||
|
||||
Livewire::test('admin.press-releases.show', ['id' => $this->pressRelease->id])
|
||||
->assertSee('Boost beenden')
|
||||
->set('boostEndReason', 'Test-Boost für die Screenshots beendet')
|
||||
->call('endBoost')
|
||||
->assertHasNoErrors();
|
||||
|
||||
expect($this->pressRelease->boosts()->active()->exists())->toBeFalse()
|
||||
->and($running->fresh()->ends_at->lessThanOrEqualTo(now()))->toBeTrue()
|
||||
->and($queued->fresh()->starts_at->lessThanOrEqualTo($queued->fresh()->ends_at))->toBeTrue()
|
||||
->and($expired->fresh()->ends_at->equalTo($expiredEnd))->toBeTrue()
|
||||
->and(Boost::query()->count())->toBe(3);
|
||||
|
||||
$event = ActivityEvent::query()->where('type', ActivityType::BoostEnded->value)->sole();
|
||||
expect($event->description)->toContain('Test-Boost für die Screenshots beendet')
|
||||
->and($event->meta['boost_ids'])->toEqualCanonicalizing([$running->id, $queued->id]);
|
||||
});
|
||||
|
||||
test('ending a boost requires a reason', function () {
|
||||
/** @var TestCase $this */
|
||||
$admin = User::factory()->create();
|
||||
$admin->assignRole('admin');
|
||||
Boost::factory()->for($this->pressRelease)->create();
|
||||
|
||||
$this->actingAs($admin);
|
||||
|
||||
Livewire::test('admin.press-releases.show', ['id' => $this->pressRelease->id])
|
||||
->set('boostEndReason', '')
|
||||
->call('endBoost')
|
||||
->assertHasErrors(['boostEndReason']);
|
||||
|
||||
expect($this->pressRelease->boosts()->active()->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
test('an editor sees the boost but cannot end it', function () {
|
||||
/** @var TestCase $this */
|
||||
$editor = User::factory()->create();
|
||||
$editor->assignRole('editor');
|
||||
Boost::factory()->for($this->pressRelease)->create();
|
||||
|
||||
$this->actingAs($editor);
|
||||
|
||||
Livewire::test('admin.press-releases.show', ['id' => $this->pressRelease->id])
|
||||
->assertSee('Gebuchte Platzierung')
|
||||
->assertDontSee('Boost beenden')
|
||||
->set('boostEndReason', 'Versuch ohne Abrechnungsrecht')
|
||||
->call('endBoost')
|
||||
->assertForbidden();
|
||||
|
||||
expect($this->pressRelease->boosts()->active()->exists())->toBeTrue();
|
||||
});
|
||||
69
tests/Feature/Auth/AdminAuditTest.php
Normal file
69
tests/Feature/Auth/AdminAuditTest.php
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Database\Seeders\RolesAndPermissionsSeeder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Berechtigungsnachweis BN-02: `audit:admins` listet jedes Konto mit Rechten
|
||||
* über den Kundenbereich hinaus als Markdown, standardmäßig ohne vollständige
|
||||
* E-Mail-Adresse, und ändert dabei nichts.
|
||||
*/
|
||||
beforeEach(function () {
|
||||
$this->seed(RolesAndPermissionsSeeder::class);
|
||||
});
|
||||
|
||||
test('the audit lists admins, editors, super admins and locked or deleted privileged accounts', function () {
|
||||
/** @var TestCase $this */
|
||||
$admin = User::factory()->create(['email' => 'betrieb@pressekonto.test', 'last_login_at' => now()]);
|
||||
$admin->assignRole('admin');
|
||||
DB::table('passkeys')->insert([
|
||||
'user_id' => $admin->id,
|
||||
'name' => 'Laptop',
|
||||
'credential_id' => 'cred-1',
|
||||
'credential' => '{}',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$editor = User::factory()->create(['email' => 'redaktion@example.test']);
|
||||
$editor->assignRole('editor');
|
||||
|
||||
$superAdmin = User::factory()->superAdmin()->create(['email' => 'alt@me.test']);
|
||||
$superAdmin->assignRole('customer');
|
||||
$superAdmin->createToken('Legacy-API-Key (importiert)');
|
||||
|
||||
$locked = User::factory()->create(['email' => 'gesperrt@example.test', 'is_active' => false]);
|
||||
$locked->assignRole('admin');
|
||||
|
||||
$deleted = User::factory()->create(['email' => 'geloescht@example.test']);
|
||||
$deleted->assignRole('admin');
|
||||
$deleted->delete();
|
||||
|
||||
$customer = User::factory()->create(['email' => 'kunde@example.test']);
|
||||
$customer->assignRole('customer');
|
||||
|
||||
$this->artisan('audit:admins')
|
||||
->expectsOutputToContain('3 Konten mit Zugang zum Admin-Bereich, davon 1 mit zweitem Faktor (TOTP oder Passkey), 1 Super-Admin, 1 Editor-Konto. Gelistet sind 5 Konten')
|
||||
->expectsOutputToContain('| '.$admin->id.' | …@pressekonto.test | admin | nein | ja | nein | ja | nein | 1 |')
|
||||
->expectsOutputToContain('| '.$superAdmin->id.' | …@me.test | customer | ja | ja | nein | ja |')
|
||||
->expectsOutputToContain('| '.$locked->id.' | …@example.test | admin | nein | gesperrt | nein | nein |')
|
||||
->expectsOutputToContain('| '.$deleted->id.' | …@example.test | admin | nein | ja | '.now()->format('d.m.Y').' | nein |')
|
||||
->doesntExpectOutputToContain('| '.$customer->id.' |')
|
||||
->doesntExpectOutputToContain('betrieb@pressekonto.test')
|
||||
->assertSuccessful();
|
||||
|
||||
expect($superAdmin->fresh()->tokens()->count())->toBe(1)
|
||||
->and($deleted->fresh()->trashed())->toBeTrue();
|
||||
});
|
||||
|
||||
test('the audit prints full email addresses only on request', function () {
|
||||
/** @var TestCase $this */
|
||||
$admin = User::factory()->create(['email' => 'betrieb@pressekonto.test']);
|
||||
$admin->assignRole('admin');
|
||||
|
||||
$this->artisan('audit:admins', ['--with-emails' => true])
|
||||
->expectsOutputToContain('| '.$admin->id.' | betrieb@pressekonto.test | admin |')
|
||||
->assertSuccessful();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue