account('admin'); $regular = $this->account('user'); $outside = $this->account('user'); $outside->companies()->detach(); $this->loginAs($admin)->get(route('system-settings.users.index')) ->assertOk()->assertSeeText($this->user->name)->assertSeeText($admin->name)->assertSeeText($regular->name) ->assertDontSeeText($outside->name) ->assertDontSee(route('system-settings.users.edit', $this->user->id), false) ->assertSee(route('system-settings.users.edit', $regular->id), false); $this->withExceptionHandling()->get(route('system-settings.users.edit', $this->user->id))->assertForbidden(); $this->patch(route('system-settings.users.update', $this->user->id), $this->payload($this->user, 'user'))->assertForbidden(); $this->delete(route('system-settings.users.destroy', $this->user->id))->assertForbidden(); } public function test_super_admin_can_assign_both_administrator_roles(): void { $target = $this->account('user'); $this->loginAs()->get(route('system-settings.users.edit', $target->id))->assertOk() ->assertSeeText('主管理员(最高权限)')->assertSee('value="admin"', false); foreach (['admin', 'super_admin'] as $role) { $this->patch(route('system-settings.users.update', $target->id), $this->payload($target, $role)) ->assertRedirect(route('system-settings.users.index')); $target = $target->fresh(); $this->assertSame([$role], $target->roles()->pluck('name')->all()); } setting()->set('system.sidebar_permissions', ['configured' => ['user_'.$target->id], 'subjects' => ['user_'.$target->id => []]]); $this->assertSame(BusinessNavigation::all(), SidebarPermissions::navigationFor($target)); $this->assertTrue(SystemAccess::isSuperAdmin($target)); $this->loginAs($target)->get(route('system-settings.users.index'))->assertOk(); $this->get(route('settings.navigation.manage'))->assertOk(); } public function test_admin_cannot_promote_a_user_or_itself(): void { $admin = $this->account('admin'); $target = $this->account('user'); $this->withExceptionHandling()->loginAs($admin); foreach (['admin', 'super_admin'] as $role) { $this->patchJson(route('system-settings.users.update', $target->id), $this->payload($target, $role)) ->assertUnprocessable()->assertJsonValidationErrors('role'); } $this->patchJson(route('system-settings.users.update', $admin->id), $this->payload($admin, 'super_admin'))->assertForbidden(); $this->assertSame(['user'], $target->roles()->pluck('name')->all()); } public function test_demotion_removes_cached_administrator_access(): void { $target = $this->account('super_admin'); $this->assertTrue($target->hasRole('super_admin')); $this->assertTrue($target->can('update-auth-users')); $this->loginAs()->patch(route('system-settings.users.update', $target->id), $this->payload($target, 'user')) ->assertRedirect(route('system-settings.users.index')); $this->assertSame('user', SystemAccess::roleName($target)); $this->assertFalse($target->fresh()->can('update-auth-users')); $this->withExceptionHandling()->loginAs($target)->get(route('system-settings.users.index'))->assertForbidden(); } public function test_legacy_user_routes_cannot_grant_administrator_identity(): void { $admin = $this->account('admin'); $target = $this->account('user'); $this->withExceptionHandling()->loginAs($admin); $role = (string) Role::where('name', 'super_admin')->value('id'); $this->patchJson(route('users.update', $target->id), ['roles' => $role])->assertForbidden(); $this->patchJson(route('profile.update', $admin->id), ['roles' => $role])->assertForbidden(); $this->postJson(route('users.store'), ['roles' => $role])->assertForbidden(); $this->patchJson(route('users.update', $this->user->id), ['name' => 'unauthorized'])->assertForbidden(); } public function test_last_super_admin_cannot_be_demoted_or_disabled(): void { $this->loginAs(); $this->patch(route('system-settings.users.update', $this->user->id), $this->payload($this->user, 'admin')) ->assertSessionHasErrors('role'); $this->patch(route('system-settings.users.update', $this->user->id), array_merge($this->payload($this->user, 'super_admin'), ['enabled' => 0])) ->assertSessionHasErrors('enabled'); $this->assertSame(['super_admin'], $this->user->roles()->pluck('name')->all()); } public function test_user_page_settings_are_a_second_level_page_and_save_for_selected_user(): void { $target = $this->account('user'); $this->loginAs()->get(route('system-settings.permissions.index'))->assertOk() ->assertSeeText('用户可访问页面')->assertSee(route('system-settings.sidebar.edit'), false); $this->get(route('system-settings.sidebar.edit', ['subject' => 'user_'.$target->id]))->assertOk() ->assertSee('aria-label="页面位置"', false)->assertSeeText('主体商标规则'); $this->patch(route('system-settings.sidebar.update'), [ 'subject' => 'user_'.$target->id, 'permissions' => ['store-management/basic-information'], ])->assertRedirect(route('system-settings.sidebar.edit', ['subject' => 'user_'.$target->id])); $this->assertSame(['store-management/basic-information'], SidebarPermissions::allowedKeys($target)); $this->get(route('system-settings.sidebar.edit', ['subject' => 'user_'.$target->id]))->assertOk()->assertSeeText('自定义权限'); } public function test_super_admin_cannot_be_limited_by_page_settings_and_regular_users_cannot_configure_access(): void { $this->withExceptionHandling()->loginAs()->patchJson(route('system-settings.sidebar.update'), [ 'subject' => 'user_'.$this->user->id, 'permissions' => [], ])->assertUnprocessable(); $regular = $this->account('user'); $this->loginAs($regular)->get(route('system-settings.permissions.index'))->assertForbidden(); $this->get(route('system-settings.users.index'))->assertForbidden(); $this->patchJson(route('system-settings.sidebar.update'), ['subject' => 'role_user', 'permissions' => []])->assertForbidden(); } public function test_child_page_navigation_contains_only_the_selected_child(): void { $regular = $this->account('user'); setting()->set('system.sidebar_permissions', ['configured' => ['user_'.$regular->id], 'subjects' => ['user_'.$regular->id => ['trademark-management/subject-trademark-rules']]]); $navigation = SidebarPermissions::navigationFor($regular); $pages = collect($navigation)->flatMap(fn ($section) => $section['pages'])->flatMap(fn ($page) => $page['children'] ?? [$page]); $this->assertSame(['subject-trademark-rules'], $pages->pluck('slug')->all()); } public function test_financial_risk_control_section_permission_includes_store_violation_alert(): void { $regular = $this->account('user'); $subject = 'user_'.$regular->id; setting()->set('system.sidebar_permissions', [ 'configured' => [$subject], 'subjects' => [$subject => ['financial-risk-control']], ]); $navigation = SidebarPermissions::navigationFor($regular); $this->assertSame(['financial-risk-control'], array_column($navigation, 'slug')); $this->assertSame( ['store-violation-alert'], array_column($navigation[0]['pages'], 'slug') ); } public function test_legacy_small_payment_permission_follows_the_page_to_financial_operations(): void { $regular = $this->account('user'); $subject = 'user_'.$regular->id; setting()->set('system.sidebar_permissions', [ 'configured' => [$subject], 'subjects' => [$subject => ['refunds/small-payment-work-orders']], ]); $this->assertSame( ['financial-operations/small-payment-work-orders'], SidebarPermissions::allowedKeys($regular) ); $navigation = SidebarPermissions::navigationFor($regular); $this->assertSame(['financial-operations'], array_column($navigation, 'slug')); $this->assertSame( ['small-payment-work-orders'], array_column($navigation[0]['pages'], 'slug') ); } public function test_legacy_refunds_section_permission_keeps_small_payment_access(): void { $regular = $this->account('user'); $subject = 'user_'.$regular->id; setting()->set('system.sidebar_permissions', [ 'configured' => [$subject], 'subjects' => [$subject => ['refunds']], ]); $this->assertSame( ['refunds', 'financial-operations/small-payment-work-orders'], SidebarPermissions::allowedKeys($regular) ); $navigation = SidebarPermissions::navigationFor($regular); $this->assertSame( ['refunds', 'financial-operations'], array_column($navigation, 'slug') ); $this->assertSame( ['small-payment-work-orders'], array_column($navigation[1]['pages'], 'slug') ); } public function test_payables_final_entries_follow_their_base_section_permissions(): void { $regular = $this->account('user'); $subject = 'user_' . $regular->id; setting()->set('system.sidebar_permissions', [ 'configured' => [$subject], 'subjects' => [$subject => ['supplier-payables', 'courier-payables']], ]); $this->assertEqualsCanonicalizing( ['supplier-payables', 'supplier-payables-final', 'courier-payables', 'courier-payables-final'], SidebarPermissions::allowedKeys($regular) ); $navigation = SidebarPermissions::navigationFor($regular); $this->assertSame( ['supplier-payables-final', 'courier-payables-final', 'supplier-payables', 'courier-payables'], array_column($navigation, 'slug') ); } public function test_payables_final_entry_follows_a_selected_child_permission(): void { $regular = $this->account('user'); $subject = 'user_' . $regular->id; setting()->set('system.sidebar_permissions', [ 'configured' => [$subject], 'subjects' => [$subject => ['supplier-payables/product-prices']], ]); $this->assertSame( ['supplier-payables/product-prices', 'supplier-payables-final'], SidebarPermissions::allowedKeys($regular) ); $navigation = SidebarPermissions::navigationFor($regular); $this->assertSame( ['supplier-payables-final', 'supplier-payables'], array_column($navigation, 'slug') ); $this->assertSame( ['product-prices'], array_column($navigation[1]['pages'], 'slug') ); } public function test_dingtalk_bound_user_cannot_be_deleted_into_an_orphaned_identity(): void { $target = $this->account('user'); DingTalkIdentity::query()->create([ 'user_id' => $target->id, 'union_id' => 'fixture-delete-guard-union', 'dingtalk_user_id' => 'fixture-delete-guard-user', 'nickname' => '在职财务', ]); $this->loginAs()->delete(route('system-settings.users.destroy', $target->id)) ->assertSessionHasErrors('user'); $this->assertNotNull($target->fresh()); $this->assertDatabaseHas('dingtalk_identities', ['user_id' => $target->id]); } private function account(string $role): User { $name = 'fixture-'.$role.'-'.User::count(); $account = User::create(['name' => $name, 'email' => $name.'@example.test', 'password' => 'fixture-unused-password', 'enabled' => true, 'locale' => 'zh-CN', 'landing_page' => 'dashboard']); $account->roles()->attach(Role::where('name', $role)->firstOrFail()->id); $account->permissions()->attach(Permission::where('name', 'read-admin-panel')->firstOrFail()->id); $account->companies()->attach($this->company->id); return $account; } private function payload(User $target, string $role): array { return ['name' => $target->name, 'email' => $target->email, 'role' => $role, 'enabled' => 1]; } }