'array', 'scout.driver' => null]); foreach (self::ALL as $name) { Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web']); } Category::firstOrCreate(['type' => Poll::class, 'categoryable_id' => null, 'categoryable_type' => null]); app(PermissionRegistrar::class)->forgetCachedPermissions(); } private function actAsAdminWith(array $permissions): Admin { $user = User::factory()->create(); $admin = Admin::factory()->create(); $admin->users()->attach($user); if ($permissions) { $role = Role::firstOrCreate(['name' => "Admin\\{$admin->id}\\matrix-role", 'guard_name' => 'web']); $role->syncPermissions($permissions); $user->assignRole($role); } $this->actingAs($user, 'web'); $this->actingAs($admin, 'admin'); session(['activeProfileType' => Admin::class, 'activeProfileId' => $admin->id]); return $admin; } private function makePoll(string $status, bool $withVote = false): Poll { $postId = DB::table('posts')->insertGetId(['postable_id' => 1, 'postable_type' => Admin::class, 'created_at' => now(), 'updated_at' => now()]); DB::table('post_translations')->insert(['post_id' => $postId, 'locale' => 'en', 'slug' => 'matrix-' . $postId, 'title' => 'Matrix poll ' . $postId, 'created_at' => now(), 'updated_at' => now()]); $poll = Poll::create(['post_id' => $postId, 'status' => $status, 'anonymous' => false]); PollQuestion::create(['poll_id' => $poll->id, 'position' => 1, 'type' => 'yes_no', 'label' => ['en' => 'Q'], 'settings' => []]); if ($withVote) { $voter = User::factory()->create(['name' => 'matrixvoter' . $poll->id]); PollResponse::create(['poll_id' => $poll->id, 'voter_id' => $voter->id, 'voter_type' => User::class, 'user_id' => $voter->id, 'dedupe_key' => $poll->dedupeKey($voter, $voter->id), 'submitted_at' => now()]); } return $poll; } /** @return array}> permissions => expected outcome per action */ public static function matrix(): array { // page create update export delete archive $row = fn ($p, $c, $u, $e, $d, $a) => ['page' => $p, 'create' => $c, 'update' => $u, 'export' => $e, 'delete' => $d, 'archive' => $a]; return [ 'no poll permission' => [[], $row(false, false, false, false, false, false)], 'view polls' => [['view polls'], $row(true, false, false, false, false, false)], 'view + create' => [['view polls', 'create polls'], $row(true, true, false, false, false, false)], 'view + update' => [['view polls', 'update polls'], $row(true, false, true, false, false, true)], 'view + export' => [['view polls', 'export polls'], $row(true, false, false, true, false, false)], 'view + delete' => [['view polls', 'delete polls'], $row(true, false, false, false, true, false)], 'manage polls' => [['manage polls'], $row(true, true, true, true, true, true)], // Posts rights give no access to polls 'manage posts only' => [['manage posts'], $row(false, false, false, false, false, false)], ]; } /** @dataProvider matrix */ public function test_permission_matrix(array $permissions, array $expected): void { foreach ($permissions as $name) { Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web']); } $this->actAsAdminWith($permissions); // Page entry (mount) Livewire::test(Manage::class)->assertStatus($expected['page'] ? 200 : 403); if (!$expected['page']) { $this->assertExportAllowed(false, $this->makePoll(Poll::STATUS_CLOSED, true)); return; } // Create $before = Poll::count(); $create = Livewire::test(Manage::class)->call('openCreateModal'); if ($expected['create']) { $create->set('newPoll.title', 'Matrix created poll')->call('createPoll')->assertHasNoErrors(); $this->assertSame($before + 1, Poll::count(), 'create'); } else { $create->assertStatus(403); $this->assertSame($before, Poll::count(), 'create'); } // Update $poll = $this->makePoll(Poll::STATUS_DRAFT); $update = Livewire::test(Manage::class)->call('openEditModal', $poll->id)->set('form.results_visibility', Poll::RESULTS_AFTER_CLOSE)->call('savePoll'); $this->assertSame($expected['update'] ? Poll::RESULTS_AFTER_CLOSE : Poll::RESULTS_NEVER, $poll->fresh()->results_visibility, 'update'); // Who voted (Results tab) and the CSV export $voted = $this->makePoll(Poll::STATUS_CLOSED, true); $results = Livewire::test(Manage::class)->call('openEditModal', $voted->id)->set('tab', 'results'); $expected['export'] ? $results->assertSee('matrixvoter' . $voted->id) : $results->assertDontSee('matrixvoter' . $voted->id); $this->assertExportAllowed($expected['export'], $voted); // Delete (a draft without votes) $draft = $this->makePoll(Poll::STATUS_DRAFT); $delete = Livewire::test(Manage::class)->call('confirmDelete', $draft->id); if ($expected['delete']) { $delete->call('deletePoll'); $this->assertSoftDeleted('polls', ['id' => $draft->id]); } else { $delete->assertStatus(403); // refused at the confirmation already $this->assertNotSoftDeleted('polls', ['id' => $draft->id]); } // Archive (a closed poll) $closed = $this->makePoll(Poll::STATUS_CLOSED, true); Livewire::test(Manage::class)->call('archivePoll', $closed->id); $this->assertSame($expected['archive'], $closed->fresh()->archived_at !== null, 'archive'); } private function assertExportAllowed(bool $allowed, Poll $poll): void { try { (new PollResultsExportController())($poll); $this->assertTrue($allowed, 'export should have been refused'); } catch (HttpException $e) { $this->assertFalse($allowed, 'export should have been allowed'); $this->assertSame(403, $e->getStatusCode()); } } public function test_non_admin_profiles_get_nothing_even_with_poll_permissions(): void { // An organization whose role somehow carries 'manage polls' $user = User::factory()->create(); $organization = Organization::factory()->create(); $user->organizations()->attach($organization->id); $role = Role::firstOrCreate(['name' => "Organization\\{$organization->id}\\matrix-role", 'guard_name' => 'web']); $role->syncPermissions(['manage polls']); $user->assignRole($role); $this->actingAs($user, 'web'); session(['activeProfileType' => Organization::class, 'activeProfileId' => $organization->id]); Livewire::test(Manage::class)->assertStatus(403); $this->assertExportAllowed(false, $this->makePoll(Poll::STATUS_CLOSED, true)); // A plain user $plain = User::factory()->create(); $this->actingAs($plain, 'web'); session(['activeProfileType' => User::class, 'activeProfileId' => $plain->id]); Livewire::test(Manage::class)->assertStatus(403); $this->assertExportAllowed(false, $this->makePoll(Poll::STATUS_CLOSED, true)); } public function test_routes_require_poll_access(): void { $routes = app('router')->getRoutes(); $this->assertContains('user.can:access polls', $routes->getByName('polls.manage')->gatherMiddleware()); $this->assertContains('user.can:access polls', $routes->getByName('polls.export')->gatherMiddleware()); } public function test_database_update_gives_manage_polls_to_every_admin_role(): void { foreach (['view posts', 'manage posts'] as $name) { Permission::firstOrCreate(['name' => $name, 'guard_name' => 'web']); } $definition = \App\Models\AdminRole::firstOrCreate(['name' => 'matrix-type'], ['label' => 'Matrix', 'permissions' => ['view posts']]); $admin = Admin::factory()->create(); $typed = Role::firstOrCreate(['name' => "Admin\\{$admin->id}\\matrix-type", 'guard_name' => 'web']); $typed->syncPermissions(['view posts']); $legacy = Role::firstOrCreate(['name' => "Admin\\{$admin->id}\\admin", 'guard_name' => 'web']); $legacy->syncPermissions(['manage posts']); $organizationRole = Role::firstOrCreate(['name' => 'Organization\\999999\\organization-manager', 'guard_name' => 'web']); $command = app(\App\Console\Commands\DatabaseUpdate::class); $command->setOutput(new \Illuminate\Console\OutputStyle(new \Symfony\Component\Console\Input\ArrayInput([]), new \Symfony\Component\Console\Output\NullOutput())); $method = new \ReflectionMethod($command, 'addPollPermissions'); $method->setAccessible(true); $method->invoke($command); $method->invoke($command); // safe to run again app(PermissionRegistrar::class)->forgetCachedPermissions(); $this->assertContains('manage polls', $definition->fresh()->permissions); $this->assertContains('view posts', $definition->fresh()->permissions); // others kept $this->assertSame(1, collect($definition->fresh()->permissions)->filter(fn ($p) => $p === 'manage polls')->count()); $this->assertTrue($typed->fresh()->hasPermissionTo('manage polls')); $this->assertTrue($legacy->fresh()->hasPermissionTo('manage polls')); $this->assertTrue($legacy->fresh()->hasPermissionTo('manage posts')); // added, not re-synced $this->assertFalse($organizationRole->fresh()->hasPermissionTo('manage polls')); // admin roles only } }