聊天室管理权限统一为职务权限

This commit is contained in:
2026-04-26 20:55:11 +08:00
parent b07f4e971a
commit 0402097b59
21 changed files with 590 additions and 395 deletions
+5
View File
@@ -370,6 +370,7 @@ class ChatControllerTest extends TestCase
$user = $this->createUserWithPositionPermissions([
PositionPermissionRegistry::USER_WARN,
PositionPermissionRegistry::USER_MUTE,
PositionPermissionRegistry::USER_BAN,
]);
$response = $this->actingAs($user)->get(route('chat.room', $room->id));
@@ -377,7 +378,9 @@ class ChatControllerTest extends TestCase
$response->assertOk();
$response->assertSee('⚠️ 警告', false);
$response->assertSee('🔇 禁言', false);
$response->assertSee('⛔ 封号', false);
$response->assertDontSee('🚫 踢出', false);
$response->assertDontSee('🌐 封IP', false);
$response->assertDontSee('🧊 冻结', false);
}
@@ -395,6 +398,8 @@ class ChatControllerTest extends TestCase
$response->assertDontSee('⚠️ 警告', false);
$response->assertDontSee('🚫 踢出', false);
$response->assertDontSee('🔇 禁言', false);
$response->assertDontSee('⛔ 封号', false);
$response->assertDontSee('🌐 封IP', false);
$response->assertDontSee('🧊 冻结', false);
}
@@ -409,14 +409,14 @@ class AdminCommandControllerTest extends TestCase
}
/**
* 测试冻结操作会给目标用户写入带 toast 的私聊提示。
* 测试封号操作会给目标用户写入带 toast 的私聊提示。
*/
public function test_freeze_message_contains_toast_notification_for_receiver(): void
public function test_ban_message_contains_toast_notification_for_receiver(): void
{
Queue::fake();
[$admin, $target, $room] = $this->createAdminCommandActors();
$response = $this->actingAs($admin)->postJson(route('command.freeze'), [
$response = $this->actingAs($admin)->postJson(route('command.ban'), [
'username' => $target->username,
'room_id' => $room->id,
'reason' => '严重违规',
@@ -424,17 +424,49 @@ class AdminCommandControllerTest extends TestCase
$response->assertOk()->assertJson([
'status' => 'success',
'message' => "冻结 {$target->username} 的账号",
'message' => "封禁 {$target->username} 的账号",
]);
$this->assertSame(-1, (int) $target->fresh()->user_level);
$privateMessage = $this->findPrivateSystemMessage($room->id, $target->username, '已冻结你的账号');
$privateMessage = $this->findPrivateSystemMessage($room->id, $target->username, '已封禁你的账号');
$this->assertNotNull($privateMessage);
$this->assertSame('🧊 账号已冻结', $privateMessage['toast_notification']['title'] ?? null);
$this->assertSame('🧊', $privateMessage['toast_notification']['icon'] ?? null);
$this->assertSame('#3b82f6', $privateMessage['toast_notification']['color'] ?? null);
$this->assertSame(' 账号已封禁', $privateMessage['toast_notification']['title'] ?? null);
$this->assertSame('', $privateMessage['toast_notification']['icon'] ?? null);
$this->assertSame('#991b1b', $privateMessage['toast_notification']['color'] ?? null);
Queue::assertPushed(SaveMessageJob::class, 2);
}
/**
* 测试封IP操作会给目标用户写入带 toast 的私聊提示。
*/
public function test_banip_message_contains_toast_notification_for_receiver(): void
{
Queue::fake();
[$admin, $target, $room] = $this->createAdminCommandActors();
$target->forceFill(['last_ip' => '192.168.1.100'])->save();
$response = $this->actingAs($admin)->postJson(route('command.banip'), [
'username' => $target->username,
'room_id' => $room->id,
'reason' => '恶意刷屏',
]);
$response->assertOk()->assertJson([
'status' => 'success',
'message' => "已封禁 {$target->username} 的 IP 与账号",
]);
$this->assertSame(-1, (int) $target->fresh()->user_level);
$this->assertSame(1, Redis::sismember('banned_ips', '192.168.1.100'));
$privateMessage = $this->findPrivateSystemMessage($room->id, $target->username, '已封禁你的 IP 并冻结账号');
$this->assertNotNull($privateMessage);
$this->assertSame('🌐 IP 已封禁', $privateMessage['toast_notification']['title'] ?? null);
$this->assertSame('🌐', $privateMessage['toast_notification']['icon'] ?? null);
$this->assertSame('#7c2d12', $privateMessage['toast_notification']['color'] ?? null);
Queue::assertPushed(SaveMessageJob::class, 2);
}
@@ -466,6 +498,64 @@ class AdminCommandControllerTest extends TestCase
]);
}
/**
* 测试缺少封号权限的职务用户会被拒绝。
*/
public function test_position_user_without_ban_permission_cannot_ban_target(): void
{
$admin = $this->createPositionedManager([
PositionPermissionRegistry::USER_WARN,
]);
$target = User::factory()->create([
'user_level' => 1,
]);
$room = Room::create([
'room_name' => '无权封号房',
]);
$this->joinRoom($admin, $room);
$this->joinRoom($target, $room);
$response = $this->actingAs($admin)->postJson(route('command.ban'), [
'username' => $target->username,
'room_id' => $room->id,
'reason' => '测试',
]);
$response->assertStatus(403)->assertJson([
'status' => 'error',
'message' => '当前职务无权封禁用户',
]);
}
/**
* 测试缺少封IP权限的职务用户会被拒绝。
*/
public function test_position_user_without_banip_permission_cannot_ban_target_ip(): void
{
$admin = $this->createPositionedManager([
PositionPermissionRegistry::USER_WARN,
]);
$target = User::factory()->create([
'user_level' => 1,
]);
$room = Room::create([
'room_name' => '无权封IP房',
]);
$this->joinRoom($admin, $room);
$this->joinRoom($target, $room);
$response = $this->actingAs($admin)->postJson(route('command.banip'), [
'username' => $target->username,
'room_id' => $room->id,
'reason' => '测试',
]);
$response->assertStatus(403)->assertJson([
'status' => 'error',
'message' => '当前职务无权封禁 IP 用户',
]);
}
/**
* 测试不能处理部门位阶更高的目标用户。
*/
@@ -645,7 +735,8 @@ class AdminCommandControllerTest extends TestCase
PositionPermissionRegistry::USER_WARN,
PositionPermissionRegistry::USER_MUTE,
PositionPermissionRegistry::USER_KICK,
PositionPermissionRegistry::USER_FREEZE,
PositionPermissionRegistry::USER_BAN,
PositionPermissionRegistry::USER_BANIP,
]);
$admin->load('activePosition.position.department');
$target = User::factory()->create([
@@ -47,6 +47,8 @@ class AdminPositionPermissionTest extends TestCase
'permissions' => [
PositionPermissionRegistry::ROOM_ANNOUNCEMENT,
PositionPermissionRegistry::ROOM_PUBLIC_BROADCAST,
PositionPermissionRegistry::USER_BAN,
PositionPermissionRegistry::USER_BANIP,
],
]);
@@ -59,6 +61,8 @@ class AdminPositionPermissionTest extends TestCase
$this->assertSame([
PositionPermissionRegistry::ROOM_ANNOUNCEMENT,
PositionPermissionRegistry::ROOM_PUBLIC_BROADCAST,
PositionPermissionRegistry::USER_BAN,
PositionPermissionRegistry::USER_BANIP,
], $position->permissions);
}
@@ -195,7 +199,9 @@ class AdminPositionPermissionTest extends TestCase
$response->assertSee('默认礼包总量');
$response->assertSee('默认礼包份数');
$response->assertSee('警告用户');
$response->assertSee('冻结用户');
$response->assertSee('封号用户');
$response->assertSee('封IP');
$response->assertSee('聊天室管理动作已统一按职务权限控制');
}
/**
@@ -39,6 +39,13 @@ class AdminSystemControllerTest extends TestCase
$response->assertDontSee('wechat_bot_config');
$response->assertDontSee('chatbot_max_gold');
$response->assertDontSee('levelexp');
$response->assertDontSee('level_warn');
$response->assertDontSee('level_mute');
$response->assertDontSee('level_kick');
$response->assertDontSee('level_announcement');
$response->assertDontSee('level_ban');
$response->assertDontSee('level_banip');
$response->assertDontSee('level_freeze');
$response->assertSee('maxlevel');
$response->assertSee('superlevel');
}
@@ -55,6 +62,13 @@ class AdminSystemControllerTest extends TestCase
'sys_name' => '新版聊天室',
'sys_notice' => '新的公共公告',
'levelexp' => '20,80,180',
'level_warn' => '40',
'level_mute' => '50',
'level_kick' => '60',
'level_announcement' => '65',
'level_ban' => '80',
'level_banip' => '90',
'level_freeze' => '95',
'maxlevel' => '88',
'superlevel' => '666',
'smtp_host' => 'attacker.smtp.example',
@@ -79,6 +93,34 @@ class AdminSystemControllerTest extends TestCase
'alias' => 'levelexp',
'body' => '10,50,150',
]);
$this->assertDatabaseHas('sysparam', [
'alias' => 'level_warn',
'body' => '5',
]);
$this->assertDatabaseHas('sysparam', [
'alias' => 'level_mute',
'body' => '50',
]);
$this->assertDatabaseHas('sysparam', [
'alias' => 'level_kick',
'body' => '60',
]);
$this->assertDatabaseHas('sysparam', [
'alias' => 'level_announcement',
'body' => '60',
]);
$this->assertDatabaseHas('sysparam', [
'alias' => 'level_ban',
'body' => '80',
]);
$this->assertDatabaseHas('sysparam', [
'alias' => 'level_banip',
'body' => '90',
]);
$this->assertDatabaseHas('sysparam', [
'alias' => 'level_freeze',
'body' => '14',
]);
$this->assertDatabaseHas('sysparam', [
'alias' => 'maxlevel',
'body' => '88',
@@ -147,6 +189,13 @@ class AdminSystemControllerTest extends TestCase
'sys_name' => '原始聊天室',
'sys_notice' => '原始公告',
'levelexp' => '10,50,150',
'level_warn' => '5',
'level_mute' => '50',
'level_kick' => '60',
'level_announcement' => '60',
'level_ban' => '80',
'level_banip' => '90',
'level_freeze' => '14',
'maxlevel' => '99',
'superlevel' => '100',
'smtp_host' => 'owner.smtp.example',
+88 -124
View File
@@ -8,11 +8,15 @@
namespace Tests\Feature;
use App\Enums\CurrencySource;
use App\Models\Department;
use App\Models\Position;
use App\Models\Room;
use App\Models\Sysparam;
use App\Models\User;
use App\Models\UserCurrencyLog;
use App\Models\UserPosition;
use App\Services\ChatUserPresenceService;
use App\Support\PositionPermissionRegistry;
use Carbon\Carbon;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
@@ -39,10 +43,6 @@ class UserControllerTest extends TestCase
Redis::flushall();
Sysparam::updateOrCreate(['alias' => 'superlevel'], ['body' => '100']);
Sysparam::updateOrCreate(['alias' => 'level_kick'], ['body' => '15']);
Sysparam::updateOrCreate(['alias' => 'level_mute'], ['body' => '15']);
Sysparam::updateOrCreate(['alias' => 'level_ban'], ['body' => '15']);
Sysparam::updateOrCreate(['alias' => 'level_banip'], ['body' => '15']);
Sysparam::updateOrCreate(['alias' => 'smtp_enabled'], ['body' => '1']); // Allow email changing in tests
}
@@ -294,6 +294,51 @@ class UserControllerTest extends TestCase
->assertJsonPath('data.bank_jjb_can_reveal', false);
}
/**
* 测试拥有封IP职务权限的用户可以查看名片中的管理员网络信息。
*/
public function test_position_user_with_banip_permission_can_view_admin_network_info(): void
{
$viewer = $this->createUserWithPositionPermissions([
PositionPermissionRegistry::USER_BANIP,
]);
$target = User::factory()->create([
'username' => 'nettarget',
'first_ip' => '10.0.0.1',
'previous_ip' => '10.0.0.2',
'last_ip' => '10.0.0.3',
]);
$response = $this->actingAs($viewer)->getJson("/user/{$target->username}");
$response->assertOk()
->assertJsonPath('data.first_ip', '10.0.0.1')
->assertJsonPath('data.last_ip', '10.0.0.2')
->assertJsonPath('data.login_ip', '10.0.0.3');
}
/**
* 测试普通用户查看别人名片时不会拿到管理员网络信息字段。
*/
public function test_user_without_banip_permission_cannot_view_admin_network_info(): void
{
$viewer = User::factory()->create();
$target = User::factory()->create([
'username' => 'hidden-net',
'first_ip' => '10.0.1.1',
'previous_ip' => '10.0.1.2',
'last_ip' => '10.0.1.3',
]);
$response = $this->actingAs($viewer)->getJson("/user/{$target->username}");
$response->assertOk()
->assertJsonMissingPath('data.first_ip')
->assertJsonMissingPath('data.last_ip')
->assertJsonMissingPath('data.login_ip')
->assertJsonMissingPath('data.location');
}
/**
* 测试不改邮箱时可以正常更新个人资料。
*/
@@ -510,126 +555,6 @@ class UserControllerTest extends TestCase
$this->assertTrue(Hash::check('newpassword123', $user->password));
}
public function test_admin_can_kick_user()
{
$admin = User::factory()->create(['username' => 'admin', 'user_level' => 20]);
$target = User::factory()->create(['username' => 'target', 'user_level' => 1]);
$room = Room::create(['id' => 1, 'room_name' => 'Test Room', 'room_owner' => 'someone']);
$this->actingAs($admin);
$response = $this->postJson("/user/{$target->username}/kick", [
'room_id' => $room->id,
]);
$response->assertStatus(200)
->assertJsonPath('status', 'success');
}
public function test_low_level_user_cannot_kick()
{
$user = User::factory()->create(['username' => 'user', 'user_level' => 1]);
$target = User::factory()->create(['username' => 'target', 'user_level' => 1]);
$room = Room::create(['id' => 1, 'room_name' => 'Test Room', 'room_owner' => 'someone']);
$this->actingAs($user);
$response = $this->postJson("/user/{$target->username}/kick", [
'room_id' => $room->id,
]);
$response->assertStatus(403);
}
public function test_room_master_can_kick()
{
$user = User::factory()->create(['username' => 'user', 'user_level' => 2]);
$target = User::factory()->create(['username' => 'target', 'user_level' => 1]);
$room = Room::create(['id' => 1, 'room_name' => 'Test Room', 'room_owner' => 'user']); // Master is 'user'
$this->actingAs($user);
$response = $this->postJson("/user/{$target->username}/kick", [
'room_id' => $room->id,
]);
if ($response->status() !== 200) {
dump($response->json());
}
$response->assertStatus(200);
}
public function test_cannot_kick_higher_level()
{
$admin = User::factory()->create(['username' => 'admin', 'user_level' => 20]);
$superadmin = User::factory()->create(['username' => 'superadmin', 'user_level' => 100]);
$room = Room::create(['id' => 1, 'room_name' => 'Test Room', 'room_owner' => 'someone']);
$this->actingAs($admin);
$response = $this->postJson("/user/{$superadmin->username}/kick", [
'room_id' => $room->id,
]);
$response->assertStatus(403);
}
public function test_admin_can_mute_user()
{
$admin = User::factory()->create(['username' => 'admin', 'user_level' => 20]);
$target = User::factory()->create(['username' => 'target', 'user_level' => 1]);
$room = Room::create(['id' => 1, 'room_name' => 'Test Room', 'room_owner' => 'someone']);
Redis::shouldReceive('setex')->once();
$this->actingAs($admin);
$response = $this->postJson("/user/{$target->username}/mute", [
'room_id' => $room->id,
'duration' => 10,
]);
$response->assertStatus(200);
}
public function test_admin_can_ban_user()
{
$admin = User::factory()->create(['username' => 'admin', 'user_level' => 20]);
$target = User::factory()->create(['username' => 'target', 'user_level' => 1]);
$room = Room::create(['id' => 1, 'room_name' => 'Test Room', 'room_owner' => 'someone']);
$this->actingAs($admin);
$response = $this->postJson("/user/{$target->username}/ban", [
'room_id' => $room->id,
]);
$response->assertStatus(200);
$target->refresh();
$this->assertEquals(-1, $target->user_level);
}
public function test_admin_can_ban_ip()
{
$admin = User::factory()->create(['username' => 'admin', 'user_level' => 20]);
$target = User::factory()->create(['username' => 'target', 'user_level' => 1, 'last_ip' => '192.168.1.100']);
$room = Room::create(['id' => 1, 'room_name' => 'Test Room', 'room_owner' => 'someone']);
Redis::shouldReceive('sadd')->with('banned_ips', '192.168.1.100')->once();
$this->actingAs($admin);
$response = $this->postJson("/user/{$target->username}/banip", [
'room_id' => $room->id,
]);
$response->assertStatus(200);
$target->refresh();
$this->assertEquals(-1, $target->user_level);
}
/**
* 让指定用户先进入聊天室,满足“仅在线用户可设置状态”的前置条件。
*/
@@ -646,4 +571,43 @@ class UserControllerTest extends TestCase
return $room;
}
/**
* 创建带指定职务权限的测试用户。
*
* @param list<string> $permissions
*/
private function createUserWithPositionPermissions(array $permissions): User
{
$user = User::factory()->create([
'user_level' => 88,
]);
$department = Department::create([
'name' => '资料权限部'.$user->id,
'rank' => 88,
'color' => '#4f46e5',
'sort_order' => 1,
'description' => '资料权限测试部门',
]);
$position = Position::create([
'department_id' => $department->id,
'name' => '资料权限职务'.$user->id,
'icon' => '🛡️',
'rank' => 88,
'level' => 88,
'sort_order' => 1,
'permissions' => $permissions,
]);
UserPosition::create([
'user_id' => $user->id,
'position_id' => $position->id,
'appointed_at' => now(),
'is_active' => true,
]);
return $user->fresh();
}
}