Snapshot of main at 9f39a6e1e (2026-11-02)

This commit is contained in:
Ronald Huynen committed 2026-11-02 10:41:47 +01:00
commit 6ccf1792d7
2580 files changed
+1040202

No files matched your search

+230
View File
@@ -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());
}
}