CluPilotCloud/tests/Feature/Admin/RbacMoveTest.php

77 lines
3.4 KiB
PHP

<?php
use App\Models\Customer;
use App\Models\Operator;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
it('moves every permission and role to the operator guard, leaving none behind', function () {
expect(Permission::where('guard_name', 'web')->count())->toBe(0)
->and(Role::where('guard_name', 'web')->count())->toBe(0)
->and(Permission::where('guard_name', 'operator')->count())->toBe(17)
->and(Role::where('guard_name', 'operator')->count())->toBe(6);
});
it('keeps Owner holding every permission after the move', function () {
$owner = Role::findByName('Owner', 'operator');
expect($owner->permissions()->count())->toBe(Permission::count());
});
it('lets an operator use a console capability and a user not', function () {
$operator = Operator::factory()->role('Owner')->create();
expect($operator->can('console.view'))->toBeTrue()
->and(method_exists(User::factory()->create(), 'hasRole'))->toBeFalse();
});
it('carries an existing operator user across with password and two-factor intact', function () {
// Simulates the pre-migration state on a live server.
$hash = bcrypt('altes-passwort');
DB::table('users')->insert([
'name' => 'Alt', 'email' => 'alt@clupilot.test', 'password' => $hash,
'two_factor_secret' => 'geheimnis', 'is_admin' => true,
'created_at' => now(), 'updated_at' => now(),
]);
$userId = DB::table('users')->where('email', 'alt@clupilot.test')->value('id');
DB::table('model_has_roles')->insert([
'role_id' => DB::table('roles')->where('name', 'Owner')->value('id'),
'model_type' => User::class, 'model_id' => $userId,
]);
// Re-run only the move migration against this hand-built state.
(require base_path('database/migrations/2026_07_29_100000_move_rbac_to_operator_guard.php'))->up();
$operator = Operator::where('email', 'alt@clupilot.test')->first();
expect($operator)->not->toBeNull()
->and($operator->password)->toBe($hash)
->and($operator->two_factor_secret)->toBe('geheimnis')
->and($operator->hasRole('Owner'))->toBeTrue()
// Not mixed: the users row is gone.
->and(DB::table('users')->where('email', 'alt@clupilot.test')->exists())->toBeFalse();
});
it('refuses to delete a users row that has a customer on it', function () {
// `users` has no `customer_id` column (checked against the real schema) —
// the link runs the other way, `customers.user_id`, exactly what
// Customer::ensureUser() writes when it attaches a portal login.
$customer = Customer::factory()->create();
$user = User::factory()->create(['email' => 'beides@clupilot.test']);
DB::table('customers')->where('id', $customer->id)->update(['user_id' => $user->id]);
DB::table('model_has_roles')->insert([
'role_id' => DB::table('roles')->where('name', 'Owner')->value('id'),
'model_type' => User::class, 'model_id' => $user->id,
]);
// Aborting is the point: silently deleting would take billing data with it.
try {
(require base_path('database/migrations/2026_07_29_100000_move_rbac_to_operator_guard.php'))->up();
$this->fail('The migration should have refused.');
} catch (RuntimeException $e) {
expect($e->getMessage())->toContain('beides@clupilot.test');
}
});