Snapshot of main at 9f39a6e1e (2026-11-02)
This commit is contained in:
commit
6ccf1792d7
2580 files changed
+1040202
No files matched your search
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Http\Livewire\Profiles\Create;
|
||||
use App\Models\Admin;
|
||||
use App\Models\AdminRole;
|
||||
use App\Models\User;
|
||||
use App\Traits\ProfilePermissionTrait;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Spatie\Permission\PermissionRegistrar;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Admin profiles had two role systems: the legacy role Admin\{id}\admin (all permissions at creation time, never
|
||||
* synced) and the roles from admin_roles (administrator, content-manager, ...). Some checks only accepted the
|
||||
* legacy role and refused admins whose admin_roles role allows the action. Now every check uses the gate, new
|
||||
* admin profiles get admin_settings.default_admin_role, and admin-roles:migrate replaces the legacy roles.
|
||||
*/
|
||||
class AdminRolesTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
// Keep the permission cache out of the Redis store the dev site shares; a cached set from an earlier
|
||||
// (rolled back) test would hide the permissions created below
|
||||
config(['permission.cache.store' => 'array']);
|
||||
app(PermissionRegistrar::class)->initializeCache();
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
|
||||
// The test database has the admin_roles definitions but no permissions
|
||||
foreach (AdminRole::all() as $definition) {
|
||||
foreach ($definition->permissions ?? [] as $name) {
|
||||
Permission::findOrCreate($name, 'web');
|
||||
}
|
||||
}
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
}
|
||||
|
||||
private function actAsAdminProfile(User $user, Admin $admin): void
|
||||
{
|
||||
$this->actingAs($user, 'web');
|
||||
session(['activeProfileType' => Admin::class, 'activeProfileId' => $admin->id, 'active_guard' => 'admin']);
|
||||
}
|
||||
|
||||
private function checks(): object
|
||||
{
|
||||
return new class {
|
||||
use ProfilePermissionTrait;
|
||||
|
||||
public function profiles(): bool { return (bool) $this->getCanManageProfiles(); }
|
||||
public function accounts(): bool { return (bool) $this->getCanManageAccounts(); }
|
||||
};
|
||||
}
|
||||
|
||||
public function test_an_administrator_without_the_legacy_role_may_manage_profiles_and_accounts(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$admin = Admin::factory()->create();
|
||||
AdminRole::where('name', 'administrator')->firstOrFail()->assignScopedTo($user, $admin->id);
|
||||
$this->actAsAdminProfile($user, $admin);
|
||||
|
||||
$this->assertTrue($this->checks()->profiles());
|
||||
$this->assertTrue($this->checks()->accounts());
|
||||
}
|
||||
|
||||
public function test_a_role_without_the_permission_is_refused(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$admin = Admin::factory()->create();
|
||||
$definition = AdminRole::where('name', 'content-editor')->firstOrFail();
|
||||
$this->assertNotContains('manage profiles', $definition->permissions);
|
||||
$definition->assignScopedTo($user, $admin->id);
|
||||
$this->actAsAdminProfile($user, $admin);
|
||||
|
||||
$this->assertFalse($this->checks()->profiles());
|
||||
$this->assertFalse($this->checks()->accounts());
|
||||
}
|
||||
|
||||
public function test_a_role_on_another_admin_profile_does_not_count(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
[$mine, $other] = Admin::factory()->count(2)->create();
|
||||
AdminRole::where('name', 'administrator')->firstOrFail()->assignScopedTo($user, $other->id);
|
||||
$this->actAsAdminProfile($user, $mine);
|
||||
|
||||
$this->assertFalse($this->checks()->profiles());
|
||||
}
|
||||
|
||||
public function test_the_global_super_admin_role_grants_nothing_without_a_scoped_role(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->assignRole(Role::findOrCreate('super-admin', 'web'));
|
||||
$admin = Admin::factory()->create();
|
||||
$this->actAsAdminProfile($user, $admin);
|
||||
|
||||
$this->assertFalse($this->checks()->profiles());
|
||||
$this->assertFalse($user->can('manage admins'));
|
||||
}
|
||||
|
||||
public function test_a_new_admin_profile_gives_the_linked_user_the_default_role_only(): void
|
||||
{
|
||||
Mail::fake();
|
||||
config([config('app.timebank_config') . '.admin_settings.default_admin_role' => 'site-editor']);
|
||||
$user = User::factory()->create();
|
||||
$data = Admin::factory()->raw(['password' => 'secret-password', 'linkUser' => $user->id]);
|
||||
|
||||
$create = new \ReflectionMethod(Create::class, 'createAdminProfile');
|
||||
$admin = $create->invoke(app(Create::class), $data);
|
||||
|
||||
$user->refresh();
|
||||
$this->assertTrue($user->hasRole("Admin\\{$admin->id}\\site-editor"));
|
||||
$this->assertFalse($user->hasRole("Admin\\{$admin->id}\\admin"), 'no legacy role');
|
||||
$this->assertFalse($user->hasRole('admin'), 'no global admin role');
|
||||
$this->assertEqualsCanonicalizing(
|
||||
AdminRole::where('name', 'site-editor')->first()->permissions,
|
||||
Role::findByName("Admin\\{$admin->id}\\site-editor", 'web')->permissions->pluck('name')->all()
|
||||
);
|
||||
}
|
||||
|
||||
private function legacyRole(Admin $admin, array $holders): Role
|
||||
{
|
||||
$role = Role::create(['name' => "Admin\\{$admin->id}\\admin", 'guard_name' => 'web']);
|
||||
$role->syncPermissions(Permission::all());
|
||||
foreach ($holders as $holder) {
|
||||
$holder->assignRole($role);
|
||||
}
|
||||
app(PermissionRegistrar::class)->forgetCachedPermissions();
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
public function test_the_migration_gives_holders_the_default_role_then_removes_the_legacy_role(): void
|
||||
{
|
||||
$admin = Admin::factory()->create();
|
||||
[$onlyLegacy, $both] = User::factory()->count(2)->create();
|
||||
$administrator = AdminRole::where('name', 'administrator')->firstOrFail();
|
||||
$administrator->assignScopedTo($both, $admin->id);
|
||||
$this->legacyRole($admin, [$onlyLegacy, $both]);
|
||||
|
||||
$this->artisan('admin-roles:migrate')->assertExitCode(0);
|
||||
|
||||
$target = "Admin\\{$admin->id}\\administrator";
|
||||
$this->assertTrue($onlyLegacy->fresh()->hasRole($target), 'the holder with only the legacy role keeps access');
|
||||
$this->assertTrue($both->fresh()->hasRole($target));
|
||||
$this->assertNull(Role::where('name', "Admin\\{$admin->id}\\admin")->first(), 'legacy role removed');
|
||||
$this->assertEqualsCanonicalizing($administrator->permissions, Role::findByName($target, 'web')->permissions->pluck('name')->all());
|
||||
|
||||
// A second run changes nothing
|
||||
$this->artisan('admin-roles:migrate')->expectsOutputToContain('nothing to do')->assertExitCode(0);
|
||||
}
|
||||
|
||||
public function test_a_dry_run_changes_nothing(): void
|
||||
{
|
||||
$admin = Admin::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$this->legacyRole($admin, [$user]);
|
||||
|
||||
$this->artisan('admin-roles:migrate', ['--dry-run' => true])
|
||||
->expectsOutputToContain("user #{$user->id} gets Admin\\{$admin->id}\\administrator")
|
||||
->assertExitCode(0);
|
||||
|
||||
$this->assertNotNull(Role::where('name', "Admin\\{$admin->id}\\admin")->first());
|
||||
$this->assertFalse($user->fresh()->hasRole("Admin\\{$admin->id}\\administrator"));
|
||||
}
|
||||
|
||||
public function test_without_a_definition_for_the_default_role_nothing_changes(): void
|
||||
{
|
||||
config([config('app.timebank_config') . '.admin_settings.default_admin_role' => 'no-such-role']);
|
||||
$admin = Admin::factory()->create();
|
||||
$user = User::factory()->create();
|
||||
$this->legacyRole($admin, [$user]);
|
||||
|
||||
$this->artisan('admin-roles:migrate')->expectsOutputToContain("'no-such-role'")->assertExitCode(1);
|
||||
|
||||
$this->assertNotNull(Role::where('name', "Admin\\{$admin->id}\\admin")->first());
|
||||
$this->assertTrue($user->fresh()->hasRole("Admin\\{$admin->id}\\admin"));
|
||||
}
|
||||
|
||||
public function test_upgrade_verify_warns_about_legacy_roles(): void
|
||||
{
|
||||
$this->legacyRole(Admin::factory()->create(), []);
|
||||
|
||||
$this->artisan('upgrade:verify', ['--only' => 'permissions'])
|
||||
->expectsOutputToContain('legacy roles')
|
||||
->assertExitCode(0);
|
||||
}
|
||||
|
||||
public function test_the_settings_form_opens_for_a_role_with_admin_settings_but_without_manage_admins(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$admin = Admin::factory()->create();
|
||||
// The migration's definitions predate the admin capabilities that database:update adds
|
||||
$definition = AdminRole::where('name', 'content-editor')->firstOrFail();
|
||||
$this->assertNotContains('manage admins', $definition->permissions);
|
||||
Permission::findOrCreate('admin settings', 'web');
|
||||
$definition->update(['permissions' => array_values(array_unique([...$definition->permissions, 'admin settings']))]);
|
||||
$definition->assignScopedTo($user, $admin->id);
|
||||
$this->actAsAdminProfile($user, $admin);
|
||||
|
||||
\Livewire\Livewire::test(\App\Http\Livewire\Profile\UpdateSettingsForm::class)->assertStatus(200);
|
||||
}
|
||||
|
||||
public function test_the_flat_admin_role_migration_only_grants_on_linked_admin_profiles(): void
|
||||
{
|
||||
[$mine, $other] = Admin::factory()->count(2)->create();
|
||||
[$linked, $unlinked] = User::factory()->count(2)->create();
|
||||
$mine->users()->attach($linked->id);
|
||||
$flat = Role::findOrCreate('admin', 'web');
|
||||
$linked->assignRole($flat);
|
||||
$unlinked->assignRole($flat);
|
||||
|
||||
$command = app(\App\Console\Commands\DatabaseUpdate::class);
|
||||
$command->setLaravel(app());
|
||||
$output = new \Symfony\Component\Console\Output\BufferedOutput();
|
||||
$command->setOutput(new \Illuminate\Console\OutputStyle(new \Symfony\Component\Console\Input\ArrayInput([]), $output));
|
||||
(new \ReflectionMethod($command, 'migrateFlatAdminRoleToScoped'))->invoke($command);
|
||||
|
||||
$scoped = fn (User $user) => $user->fresh()->roles->pluck('name')->filter(fn ($n) => str_starts_with($n, 'Admin\\'))->values()->all();
|
||||
$this->assertSame(["Admin\\{$mine->id}\\administrator"], $scoped($linked), 'only the linked admin profile');
|
||||
$this->assertSame([], $scoped($unlinked), 'no admin profile link: no role on any admin profile');
|
||||
$this->assertStringContainsString("id:{$unlinked->id}) has the flat admin role but no admin profile link", $output->fetch());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user