presseportale/tests/Feature/Admin/AdminGrantTest.php
Kevin Adametz 1d76f5a733 auth: Firmenrechte nur aus nachgewiesener Adresse, 2FA auf allen Login-Wegen (Audit Block 1, Deploy A)
Audit vom 11.09. gegen HEAD geprüft, Entwicklungsplan dazu unter
dev/2026-09-11-audit/ENTWICKLUNGSPLAN.md. Dieser Commit ist Deploy A:
die sechs kleinen Punkte aus Block 1 ohne Schemaänderung.

F01: Der Fortify-Endpunkt PUT /user/profile-information tauschte die
E-Mail sofort und setzte die Verifizierung zurück; der Login-Sync vergab
danach Firmenrechte allein nach E-Mail-Treffer. Feature abgeschaltet
(die Kunden-UI hat den pending_email-Prozess), und der Sync verlangt
jetzt ein verifiziertes, aktives Konto – für alle Aufrufer.

F02: Google-Login übergibt Konten mit bestätigtem TOTP in die Challenge,
wie Passwort- und Magic-Link-Login.

F03: showRecoveryCodes ist gesperrt; ein Client-Update konnte die
Recovery-Codes entschlüsseln lassen.

F08: Nutzer-Detailroute unter can:users:manage (persistente Livewire-
Middleware), Gutschriften autorisieren zusätzlich in der Action.

F09: Gate::before greift nur für aktive Super-Admins; deaktivierte
fallen auf die Gates mit is_active-Prüfung zurück.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-18 10:08:55 +00:00

126 lines
5.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\ActivityType;
use App\Enums\CreditTransactionType;
use App\Enums\SinglePurchaseStatus;
use App\Enums\SinglePurchaseType;
use App\Models\ActivityEvent;
use App\Models\User;
use App\Services\Billing\AdminGrantService;
use App\Services\Billing\CreditWalletService;
use Database\Seeders\RolesAndPermissionsSeeder;
use Livewire\Volt\Volt as LivewireVolt;
use Spatie\Permission\PermissionRegistrar;
use Tests\TestCase;
/**
* Admin-Gutschriften ohne Zahlung (Admin ▸ Benutzer ▸ Guthaben & Kontingent):
* Credits als Grant ins Wallet-Ledger, freie PMs als 0-€-Einzelkäufe —
* beides protokolliert im Aktivitäts-Verlauf.
*/
test('an admin can grant credits without a payment', function () {
$admin = User::factory()->create(['name' => 'Admin Ada']);
$user = User::factory()->create();
app(AdminGrantService::class)->grantCredits($user, 500, $admin, 'Interne Nutzung');
expect(app(CreditWalletService::class)->balance($user))->toBe(500);
$transaction = $user->creditWallet()->sole()->transactions()->sole();
expect($transaction->type)->toBe(CreditTransactionType::Grant)
->and($transaction->amount_credits)->toBe(500)
->and($transaction->description)->toContain('Admin Ada')
->and($transaction->description)->toContain('Interne Nutzung');
$event = ActivityEvent::query()->where('type', ActivityType::AdminGrant->value)->sole();
expect($event->user_id)->toBe($user->id)
->and($event->description)->toContain('500 Credits')
->and($event->description)->toContain('ohne Zahlung')
->and($event->meta['granted_by'])->toBe($admin->id);
});
test('an admin can grant free press releases that satisfy the booking gate', function () {
config(['billing.enforce_booking' => true]);
$admin = User::factory()->create();
$user = User::factory()->create();
expect($user->hasActiveBooking())->toBeFalse();
app(AdminGrantService::class)->grantFreePressReleases($user, 3, $admin, 'Eigene PMs');
$purchases = $user->singlePurchases()->get();
expect($purchases)->toHaveCount(3)
->and($purchases->first()->type)->toBe(SinglePurchaseType::SinglePm)
->and($purchases->first()->status)->toBe(SinglePurchaseStatus::Paid)
->and($purchases->first()->price_cents)->toBe(0)
->and($purchases->first()->paid_at)->not->toBeNull();
$fresh = User::query()->find($user->id);
expect($fresh->hasActiveBooking())->toBeTrue()
->and($fresh->openSinglePurchaseCount())->toBe(3)
->and($fresh->pressReleaseQuotaRemaining())->toBe(3);
$event = ActivityEvent::query()->where('type', ActivityType::AdminGrant->value)->sole();
expect($event->description)->toContain('3 freie Pressemitteilung(en)')
->and($event->meta['free_press_releases'])->toBe(3);
});
test('the user show page grants credits and free pms through the panel', function () {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
$admin = User::factory()->create(['is_active' => true]);
$admin->assignRole('admin');
$this->actingAs($admin);
// Gutschrift auf das eigene Admin-Konto — der Live-Anwendungsfall.
LivewireVolt::test('admin.users.show', ['id' => $admin->id])
->set('grantCreditsAmount', 250)
->set('grantCreditsNote', 'Eigenbedarf')
->call('grantCredits')
->assertHasNoErrors()
->set('grantPmCount', 5)
->call('grantFreePressReleases')
->assertHasNoErrors();
expect(app(CreditWalletService::class)->balance($admin))->toBe(250)
->and($admin->singlePurchases()->grantingSubmission()->count())->toBe(5);
});
test('the grant panel is not available without the users manage permission', function () {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
$editor = User::factory()->create(['is_active' => true]);
$editor->assignRole('editor');
$this->actingAs($editor);
$this->get(route('admin.users.show', $editor->id))->assertForbidden();
});
test('grants are refused once the permission is revoked while the panel stays open (Audit F08)', function () {
/** @var TestCase $this */
$this->seed(RolesAndPermissionsSeeder::class);
$admin = User::factory()->create(['is_active' => true]);
$admin->assignRole('admin');
$this->actingAs($admin);
// Zwei geladene Komponenten (ein 403 verwirft den Livewire-Snapshot).
$credits = LivewireVolt::test('admin.users.show', ['id' => $admin->id]);
$releases = LivewireVolt::test('admin.users.show', ['id' => $admin->id]);
// Herabstufung, während die Komponenten bereits geladen sind.
$admin->syncRoles(['editor']);
app()[PermissionRegistrar::class]->forgetCachedPermissions();
$credits->set('grantCreditsAmount', 250)->call('grantCredits')->assertForbidden();
$releases->set('grantPmCount', 2)->call('grantFreePressReleases')->assertForbidden();
expect(app(CreditWalletService::class)->balance($admin))->toBe(0)
->and($admin->singlePurchases()->count())->toBe(0);
// Die Route selbst verlangt die Berechtigung – nicht nur mount().
$this->get(route('admin.users.show', $admin->id))->assertForbidden();
});