member(); $this->loginAs()->post(route('system-settings.groups.store'), ['name' => '核算组', 'members' => [$member->id]]) ->assertRedirect(route('system-settings.groups.index')); $group = UserAccessGroup::where('name', '核算组')->sole(); $this->assertSame([], $group->page_permissions); $this->assertSame([], SidebarPermissions::allowedKeys($member)); $this->assertSame(['user'], $member->roles()->pluck('name')->all()); $this->get(route('system-settings.users.index'))->assertOk()->assertSeeText('核算组'); $this->get(route('system-settings.groups.index'))->assertOk()->assertSeeText('未开放页面'); } public function test_later_user_can_be_added_to_existing_groups_from_user_management(): void { $first = $this->member(); $group = $this->group('现有核算组', $first, ['store-management/basic-information']); $group->users()->detach($first->id); $later = $this->member(); $this->loginAs()->get(route('system-settings.users.index'))->assertOk() ->assertSee(route('system-settings.groups.membership.edit', $later->id), false) ->assertSeeText('管理分组'); $this->get(route('system-settings.groups.membership.edit', $later->id))->assertOk() ->assertSeeText('现有核算组')->assertSeeText($later->name); $this->patch(route('system-settings.groups.membership.update', $later->id), ['groups' => [$group->id]]) ->assertRedirect(route('system-settings.users.index')); $this->assertTrue($group->users()->whereKey($later->id)->exists()); $this->assertSame(['store-management/basic-information'], SidebarPermissions::allowedKeys($later)); } public function test_user_membership_editor_is_company_scoped_and_super_admin_only(): void { $member = $this->member(); $group = $this->group('本公司分组', $member); $admin = $this->member('admin'); $this->withExceptionHandling()->loginAs($admin) ->get(route('system-settings.groups.membership.edit', $member->id))->assertForbidden(); $this->loginAs()->withHeaders(['X-Requested-With' => 'XMLHttpRequest']) ->get(route('system-settings.groups.membership.edit', $this->user->id))->assertNotFound(); $this->patchJson(route('system-settings.groups.membership.update', $member->id), ['groups' => [$group->id + 9999]]) ->assertUnprocessable(); $this->assertSame([$group->id], UserAccessGroup::forUser($member)->pluck('id')->all()); } public function test_multiple_group_permissions_are_unioned_and_enforced_on_real_routes(): void { $member = $this->member(); $a = $this->group('核算', $member, ['store-management/basic-information']); $b = $this->group('复核', $member, ['trademark-management/information']); $this->assertEqualsCanonicalizing(['store-management/basic-information', 'trademark-management/information'], SidebarPermissions::allowedKeys($member)); $this->withExceptionHandling()->loginAs($member)->get(route('business.pages.show', ['section' => 'company-management', 'page' => 'basic-information']))->assertForbidden(); $this->loginAs()->patch(route('system-settings.sidebar.update'), ['subject' => 'group_'.$b->id, 'permissions' => []]) ->assertRedirect(route('system-settings.sidebar.edit', ['subject' => 'group_'.$b->id])); $this->assertSame(['store-management/basic-information'], SidebarPermissions::allowedKeys($member)); $this->loginAs($member)->get(route('dashboard'))->assertRedirectContains('/workspace/store-management/basic-information'); } public function test_user_override_can_be_empty_and_can_restore_group_inheritance(): void { $member = $this->member(); $this->group('共享核算组', $member, ['store-management/basic-information']); $this->loginAs()->patch(route('system-settings.sidebar.update'), ['subject' => 'user_'.$member->id, 'permissions' => []]); $this->assertSame([], SidebarPermissions::allowedKeys($member)); $this->get(route('system-settings.sidebar.edit', ['subject' => 'user_'.$member->id]))->assertOk()->assertSeeText('恢复继承分组权限'); $this->patch(route('system-settings.sidebar.update'), ['subject' => 'user_'.$member->id, 'mode' => 'inherit']) ->assertRedirect(route('system-settings.sidebar.edit', ['subject' => 'user_'.$member->id])); $this->assertSame(['store-management/basic-information'], SidebarPermissions::allowedKeys($member)); $this->get(route('system-settings.sidebar.edit', ['subject' => 'user_'.$member->id]))->assertOk()->assertSeeText('正在继承分组:共享核算组'); } public function test_group_configuration_uses_the_existing_permissions_editor(): void { $member = $this->member(); $group = $this->group('出纳组', $member); $this->loginAs()->get(route('system-settings.sidebar.edit', ['subject' => 'group_'.$group->id]))->assertOk()->assertSeeText('分组:出纳组'); $this->patch(route('system-settings.sidebar.update'), ['subject' => 'group_'.$group->id, 'permissions' => ['fund-management']]); $this->assertSame(['fund-management'], $group->fresh()->page_permissions); $this->assertSame(['fund-management'], SidebarPermissions::allowedKeys($member)); $this->withExceptionHandling()->patchJson(route('system-settings.sidebar.update'), ['subject' => 'group_'.$group->id, 'permissions' => ['not-a-page']])->assertUnprocessable(); } public function test_group_page_access_does_not_grant_store_password_access(): void { $member = $this->member(); $this->group('店铺资料组', $member, ['store-management']); $this->mock(\App\Services\StoreAccountPasswordClient::class)->shouldNotReceive('reveal'); $this->withExceptionHandling()->loginAs($member)->postJson(route('business.store-account-passwords.reveal', ['account' => 'fixture-account']), ['purpose' => 'view'])->assertForbidden(); } public function test_group_membership_and_names_can_change_without_changing_permissions(): void { $member = $this->member(); $other = $this->member(); $group = $this->group('旧名称', $member, ['store-management']); $this->loginAs()->put(route('system-settings.groups.update', $group->id), ['name' => '新名称', 'members' => [$other->id]]) ->assertRedirect(route('system-settings.groups.index')); $this->assertSame(['store-management'], SidebarPermissions::allowedKeys($other)); $this->assertNull(SidebarPermissions::allowedKeys($member)); $this->assertSame(['store-management'], $group->fresh()->page_permissions); $this->assertSame('新名称', $group->fresh()->name); } public function test_delete_requires_an_empty_group(): void { $member = $this->member(); $group = $this->group('核算组', $member); $this->loginAs()->delete(route('system-settings.groups.destroy', $group->id))->assertSessionHasErrors('group'); $this->assertNotNull($group->fresh()); $this->put(route('system-settings.groups.update', $group->id), ['name' => '核算组']); $this->delete(route('system-settings.groups.destroy', $group->id))->assertRedirect(route('system-settings.groups.index')); $this->assertNull($group->fresh()); } public function test_only_super_admins_can_change_groups_or_group_permissions(): void { $member = $this->member('admin'); $group = $this->group('管理员业务组', $member); $this->withExceptionHandling()->loginAs($member)->get(route('system-settings.groups.index'))->assertForbidden(); $this->post(route('system-settings.groups.store'), ['name' => '不允许'])->assertForbidden(); $this->put(route('system-settings.groups.update', $group->id), ['name' => '不允许'])->assertForbidden(); $this->delete(route('system-settings.groups.destroy', $group->id))->assertForbidden(); $this->patchJson(route('system-settings.sidebar.update'), ['subject' => 'group_'.$group->id, 'permissions' => ['store-management']])->assertUnprocessable(); } public function test_group_and_member_ids_cannot_cross_company_or_grant_super_admin_membership(): void { $member = $this->member(); $member->companies()->detach(); $this->withExceptionHandling()->loginAs()->postJson(route('system-settings.groups.store'), ['name' => '跨公司', 'members' => [$member->id]])->assertUnprocessable(); $this->postJson(route('system-settings.groups.store'), ['name' => '主管理员组', 'members' => [$this->user->id]])->assertUnprocessable(); $group = $this->group('本公司', $this->member()); $this->withHeaders(['X-Requested-With' => 'XMLHttpRequest'])->getJson(route('system-settings.groups.edit', $group->id + 9999))->assertNotFound(); $otherCompany = $this->company->replicate(); $otherCompany->domain = 'group-isolation.example.test'; $otherCompany->save(); $foreignGroup = new UserAccessGroup(['name' => '其他公司分组', 'page_permissions' => ['store-management']]); $foreignGroup->company_id = $otherCompany->id; $foreignGroup->save(); $this->getJson(route('system-settings.groups.edit', $foreignGroup->id))->assertNotFound(); $this->patchJson(route('system-settings.sidebar.update'), ['subject' => 'group_'.$foreignGroup->id, 'permissions' => []])->assertUnprocessable(); $this->postJson(route('system-settings.groups.store'), ['name' => '本公司'])->assertUnprocessable(); $this->assertNull(SidebarPermissions::allowedKeys($this->user)); } private function member(string $role = 'user'): User { $name = 'member-'.User::count(); $user = User::create(['name' => $name, 'email' => $name.'@example.test', 'password' => 'fixture-only-password', 'enabled' => true, 'locale' => 'zh-CN', 'landing_page' => 'dashboard']); $user->roles()->attach(Role::where('name', $role)->sole()->id); $user->permissions()->attach(Permission::where('name', 'read-admin-panel')->sole()->id); $user->companies()->attach($this->company->id); return $user; } private function group(string $name, User $member, array $permissions = []): UserAccessGroup { $group = new UserAccessGroup(['name' => $name, 'page_permissions' => $permissions]); $group->company_id = $this->company->id; $group->save(); $group->users()->attach($member->id); return $group; } }