MenuIconTest.php 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. <?php
  2. namespace Tests\Integration;
  3. use App\Http\Controllers\admin\Menu as MenuController;
  4. use App\Models\Menu;
  5. use Illuminate\Config\Repository;
  6. use Illuminate\Database\Capsule\Manager;
  7. use Illuminate\Database\Schema\Blueprint;
  8. use Illuminate\Foundation\Application;
  9. use Illuminate\Http\JsonResponse;
  10. use Illuminate\Http\Request;
  11. use Illuminate\Support\Facades\DB;
  12. use Illuminate\Support\Facades\Facade;
  13. use Illuminate\Translation\ArrayLoader;
  14. use Illuminate\Translation\Translator;
  15. use Illuminate\Validation\Factory;
  16. use PHPUnit\Framework\TestCase;
  17. class MenuIconTest extends TestCase
  18. {
  19. private Application $app;
  20. protected function setUp(): void
  21. {
  22. Facade::clearResolvedInstances();
  23. require_once dirname(__DIR__, 2) . '/app/Helpers/helpers.php';
  24. $this->app = new Application(dirname(__DIR__, 2));
  25. $this->app->instance('config', new Repository(['app' => ['url' => 'https://admin.example.test', 'locale' => 'zh']]));
  26. Facade::setFacadeApplication($this->app);
  27. $db = new Manager($this->app);
  28. $db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => 'bot_']);
  29. $db->setAsGlobal();
  30. $db->bootEloquent();
  31. $this->app->instance('db', $db->getDatabaseManager());
  32. $this->app->bind('db.schema', fn () => $db->schema());
  33. $translator = new Translator(new ArrayLoader(), 'zh');
  34. $this->app->instance('translator', $translator);
  35. $this->app->instance('validator', new Factory($translator, $this->app));
  36. $this->app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Tests\Support\JsonResponseFactory());
  37. Request::macro('validate', function (array $rules) {
  38. return app('validator')->make($this->all(), $rules)->validate();
  39. });
  40. $db->schema()->create('menus', function (Blueprint $t) {
  41. $t->id(); $t->unsignedBigInteger('parent_id')->default(0); $t->string('title');
  42. $t->string('icon', 50)->nullable(); $t->string('uri'); $t->string('permission_name')->nullable();
  43. $t->integer('sort')->default(0); $t->integer('status')->default(1); $t->integer('type')->default(1); $t->timestamps();
  44. });
  45. (require dirname(__DIR__, 2) . '/database/migrations/2026_10_29_120000_expand_menu_icon_for_uploaded_images.php')->up();
  46. foreach (['roles' => ['name', 'display_name', 'description'], 'role_user' => ['user_id', 'role_id'], 'role_menu' => ['menu_id', 'role_id']] as $table => $fields) {
  47. $db->schema()->create($table, function (Blueprint $t) use ($fields) {
  48. $t->id();
  49. foreach ($fields as $field) $t->string($field)->nullable();
  50. $t->timestamps();
  51. });
  52. }
  53. $this->request('/admin/menu/all');
  54. }
  55. protected function tearDown(): void
  56. {
  57. DB::disconnect();
  58. Facade::clearResolvedInstances();
  59. parent::tearDown();
  60. }
  61. private function request(string $path, array $params = [], string $method = 'GET'): void
  62. {
  63. $this->app->instance('request', Request::create($path, $method, $params));
  64. }
  65. private function save(array $params = []): array
  66. {
  67. $this->request('/admin/menu/submit', $params + ['title' => '报表', 'uri' => '/reports', 'parent_id' => 0, 'status' => 1, 'sort' => 0, 'type' => 1], 'POST');
  68. return (new MenuController())->store()->getData(true);
  69. }
  70. private function allMenus(): array
  71. {
  72. $this->request('/admin/menu/all');
  73. $body = (new MenuController())->all()->getData(true);
  74. $this->assertSame(0, $body['code']);
  75. return $body['data'];
  76. }
  77. public function test_uploaded_image_roundtrips_in_parent_and_child_menu_nodes(): void
  78. {
  79. $path = '/storage/images/' . str_repeat('12345678', 8) . '.png';
  80. $url = 'https://admin.example.test' . $path;
  81. $this->assertSame(0, $this->save(['icon' => $url])['code']);
  82. $parent = (int) DB::table('menus')->value('id');
  83. $this->assertSame($path, DB::table('menus')->where('id', $parent)->value('icon'));
  84. $this->assertSame(0, $this->save(['title' => '子菜单', 'uri' => '/reports/daily', 'parent_id' => $parent, 'icon' => '/storage/images/child.webp'])['code']);
  85. $tree = $this->allMenus();
  86. $this->assertSame($url, $tree[0]['icon']);
  87. $this->assertSame('https://admin.example.test/storage/images/child.webp', $tree[0]['children'][0]['icon']);
  88. $this->assertSame('/reports/daily', $tree[0]['children'][0]['uri']);
  89. $this->assertSame(0, $this->save(['id' => $parent, 'icon' => $tree[0]['icon']])['code']);
  90. $this->assertSame($path, DB::table('menus')->where('id', $parent)->value('icon'));
  91. }
  92. public function test_icon_can_be_retained_replaced_with_cdn_url_and_cleared(): void
  93. {
  94. $this->assertSame(0, $this->save(['icon' => '/storage/images/menu.png'])['code']);
  95. $id = (int) DB::table('menus')->value('id');
  96. $this->assertSame(0, $this->save(['id' => $id, 'title' => '改名'])['code']);
  97. $this->assertSame('https://admin.example.test/storage/images/menu.png', $this->allMenus()[0]['icon']);
  98. $url = 'https://cdn.example.test/menu.png?version=2';
  99. $this->assertSame(0, $this->save(['id' => $id, 'icon' => $url])['code']);
  100. $this->assertSame($url, $this->allMenus()[0]['icon']);
  101. foreach ([null, ''] as $empty) {
  102. $this->assertSame(0, $this->save(['id' => $id, 'icon' => '/storage/images/menu.png'])['code']);
  103. $this->assertSame(0, $this->save(['id' => $id, 'icon' => $empty])['code']);
  104. $this->assertSame('', $this->allMenus()[0]['icon']);
  105. }
  106. }
  107. public function test_class_names_invalid_values_and_oversized_icons_are_rejected_without_writes(): void
  108. {
  109. foreach (['ri-home-line', 'fa fa-home', 'javascript:alert(1)', '//other.example.test/icon.png', ['icon'], 'https://cdn.example.test/' . str_repeat('x', 500)] as $value) {
  110. $this->assertNotSame(0, $this->save(['icon' => $value])['code']);
  111. $this->assertSame(0, DB::table('menus')->count());
  112. }
  113. }
  114. public function test_existing_class_icons_are_empty_and_role_menus_use_uploaded_images(): void
  115. {
  116. DB::table('menus')->insert(['id' => 1, 'parent_id' => 0, 'title' => '旧菜单', 'uri' => '/legacy', 'icon' => 'ri-home-line']);
  117. $this->assertSame(0, $this->save(['icon' => '/storage/images/allowed.png'])['code']);
  118. $id = (int) DB::table('menus')->where('uri', '/reports')->value('id');
  119. $this->assertSame('', $this->allMenus()[0]['icon']);
  120. $this->assertSame('ri-home-line', DB::table('menus')->where('id', 1)->value('icon'));
  121. DB::table('roles')->insert(['id' => 2, 'name' => 'viewer']);
  122. DB::table('role_user')->insert(['user_id' => 20, 'role_id' => 2]);
  123. DB::table('role_menu')->insert(['menu_id' => $id, 'role_id' => 2]);
  124. $this->request('/admin/menu/mineMenu');
  125. request()->user = (object) ['id' => 20];
  126. $body = (new MenuController())->mineMenu()->getData(true);
  127. $this->assertSame(0, $body['code']);
  128. $this->assertCount(1, $body['data']);
  129. $this->assertSame($id, $body['data'][0]['id']);
  130. $this->assertSame('https://admin.example.test/storage/images/allowed.png', $body['data'][0]['icon']);
  131. }
  132. }