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>
126 lines
5.1 KiB
PHP
126 lines
5.1 KiB
PHP
<?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();
|
||
});
|