instance('config', new Repository(['app' => ['url' => 'https://admin.example.test']])); Facade::setFacadeApplication($app); $db = new Manager($app); $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']); $db->setAsGlobal(); $db->bootEloquent(); $app->instance('db', $db->getDatabaseManager()); $db->schema()->create('menus', function (Blueprint $t) { $t->id(); $t->integer('parent_id')->default(0); $t->string('title'); $t->integer('sort')->default(0); $t->integer('status')->default(1); $t->integer('type')->default(1); $t->string('uri')->default(''); }); $db->schema()->create('roles', function (Blueprint $t) { $t->id(); $t->string('name'); }); foreach (['role_user' => 'user_id', 'role_menu' => 'menu_id'] as $table => $field) { $db->schema()->create($table, function (Blueprint $t) use ($field) { $t->id(); $t->integer('role_id'); $t->integer($field); }); } } protected function tearDown(): void { DB::disconnect(); Facade::clearResolvedInstances(); Model::clearBootedModels(); parent::tearDown(); } private function menu(int $id, int $sort, int $parent = 0, int $status = 1, int $type = 1): void { DB::table('menus')->insert([ 'id' => $id, 'title' => 'Menu ' . $id, 'sort' => $sort, 'parent_id' => $parent, 'status' => $status, 'type' => $type, ]); } private function role(int $id, array $menus, array $users): void { DB::table('roles')->insert(['id' => $id, 'name' => 'Role ' . $id]); foreach ($menus as $menuId) { DB::table('role_menu')->insert(['role_id' => $id, 'menu_id' => $menuId]); } foreach ($users as $userId) { DB::table('role_user')->insert(['role_id' => $id, 'user_id' => $userId]); } } private function treeIds($tree): array { $result = []; foreach ($tree as $node) { $result[] = ['id' => (int) $node['id'], 'children' => $this->treeIds($node['children'])]; } return $result; } public function test_single_and_multiple_roles_follow_admin_order_for_roots_children_and_equal_sort(): void { foreach ([[10, 30], [20, 10], [30, 10]] as [$id, $sort]) { $this->menu($id, $sort); } foreach ([[50, 30], [60, 5], [70, 5], [80, 20]] as [$id, $sort]) { $this->menu($id, $sort, 20); } $this->role(1, [10, 30, 50, 70], [20]); $this->role(2, [20, 30, 60, 70, 80], [20]); $this->role(3, [70, 50, 30, 10, 80, 60, 20], [21]); $expected = $this->treeIds(MenuService::getUserMenu(1)); $this->assertSame([20, 30, 10], array_column($expected, 'id')); $this->assertSame([60, 70, 80, 50], array_column($expected[0]['children'], 'id')); $this->assertSame($expected, $this->treeIds(MenuService::getUserMenu(20))); $this->assertSame($expected, $this->treeIds(MenuService::getUserMenu(21))); } public function test_sorting_preserves_role_permissions_existing_hidden_menus_and_menu_types(): void { $this->menu(10, 30); $this->menu(20, 10); $this->menu(30, 20, status: 2); $this->menu(40, 20, type: 2); $this->menu(50, 10, type: 2); $this->menu(60, 0, type: 2); $this->role(1, [10, 30, 40, 50], [20]); $this->role(2, [20], [21]); $this->assertSame([30, 10], array_column($this->treeIds(MenuService::getUserMenu(20)), 'id')); $this->assertSame([20], array_column($this->treeIds(MenuService::getUserMenu(21)), 'id')); $buttons = MenuService::getUserMenu(20, 2); $this->assertSame([50, 40], array_column($buttons, 'id')); $this->assertSame([], MenuService::getUserMenu(99)); $this->assertSame([], MenuService::getUserMenu(99, 2)); // Admin still uses its existing sort-only query and visible-menu filter. $this->assertSame([20, 10], array_column($this->treeIds(MenuService::getUserMenu(1)), 'id')); $this->assertSame([60, 50, 40], MenuService::getUserMenu(1, 2)->pluck('id')->all()); } }