收口聊天室安全边界并优化特效生命周期

This commit is contained in:
2026-04-25 02:52:30 +08:00
parent 4d3f4f7a4b
commit 855d031b04
26 changed files with 1219 additions and 175 deletions
+164
View File
@@ -10,6 +10,7 @@ namespace Tests\Feature;
use App\Events\MessageSent;
use App\Models\Department;
use App\Models\Gift;
use App\Models\Position;
use App\Models\Room;
use App\Models\Sysparam;
@@ -466,6 +467,26 @@ class ChatControllerTest extends TestCase
$this->assertTrue($found, 'Message not found in Redis');
}
/**
* 测试未进入当前房间时不能直接调用发言接口。
*/
public function test_send_message_requires_user_to_be_in_room(): void
{
$room = Room::create(['room_name' => 'sndbd']);
$user = User::factory()->create();
$response = $this->actingAs($user)->postJson(route('chat.send', $room->id), [
'to_user' => '大家',
'content' => '跨房间发言',
'is_secret' => false,
'font_color' => '#000000',
'action' => '',
]);
$response->assertStatus(403);
$this->assertSame([], Redis::lrange("room:{$room->id}:messages", 0, -1));
}
/**
* 测试发送接口会拦截不在白名单内的危险动作值。
*/
@@ -488,6 +509,57 @@ class ChatControllerTest extends TestCase
$response->assertJsonValidationErrors('action');
}
/**
* 测试发送接口会拒绝非标准十六进制颜色,避免 style 属性注入。
*/
public function test_send_message_rejects_invalid_font_color(): void
{
$room = Room::create(['room_name' => 'badcolor']);
$user = User::factory()->create();
$this->actingAs($user)->get(route('chat.room', $room->id));
$response = $this->actingAs($user)->postJson(route('chat.send', $room->id), [
'to_user' => '大家',
'content' => '颜色注入测试',
'is_secret' => false,
'font_color' => '#fff;evil',
'action' => '',
]);
$response->assertStatus(422);
$response->assertJsonValidationErrors('font_color');
}
/**
* 测试合法发言颜色仍可正常保存到消息与用户偏好。
*/
public function test_send_message_accepts_valid_hex_font_color(): void
{
$room = Room::create(['room_name' => 'goodcolor']);
$user = User::factory()->create();
$this->actingAs($user)->get(route('chat.room', $room->id));
$response = $this->actingAs($user)->postJson(route('chat.send', $room->id), [
'to_user' => '大家',
'content' => '合法颜色测试',
'is_secret' => false,
'font_color' => '#12AaF0',
'action' => '',
]);
$response->assertOk()->assertJson(['status' => 'success']);
$messages = Redis::lrange("room:{$room->id}:messages", 0, -1);
$message = collect($messages)
->map(fn (string $item) => json_decode($item, true))
->first(fn (array $item) => ($item['content'] ?? null) === '合法颜色测试');
$this->assertSame('#12AaF0', $message['font_color'] ?? null);
$this->assertSame('#12AaF0', $user->fresh()->s_color);
}
/**
* 测试定向消息仅广播到发送方与接收方私有频道。
*/
@@ -634,6 +706,34 @@ class ChatControllerTest extends TestCase
]);
}
/**
* 测试房间公告更新广播中的动态内容会被转义。
*/
public function test_set_announcement_escapes_html_in_system_message(): void
{
$room = Room::create(['room_name' => 'annsafe']);
$user = $this->createUserWithPositionPermissions([
PositionPermissionRegistry::ROOM_ANNOUNCEMENT,
]);
$this->actingAs($user)->get(route('chat.room', $room->id));
$response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [
'announcement' => '<img src=x onerror=alert(1)>',
]);
$response->assertOk();
$messages = Redis::lrange("room:{$room->id}:messages", 0, -1);
$systemMessage = collect($messages)
->map(fn (string $item) => json_decode($item, true))
->first(fn (array $item) => ($item['from_user'] ?? null) === '系统公告');
$this->assertNotNull($systemMessage);
$this->assertStringNotContainsString('<img src=x onerror=alert(1)>', (string) $systemMessage['content']);
$this->assertStringContainsString('&lt;img src=x onerror=alert(1)&gt;', (string) $systemMessage['content']);
}
/**
* 测试超过保留期的聊天图片会被命令清理并改成过期占位消息。
*/
@@ -745,6 +845,9 @@ class ChatControllerTest extends TestCase
$receiver = User::factory()->create(['jjb' => 100]);
$outsider = User::factory()->create();
$this->joinRoom($sender, $room);
$this->joinRoom($receiver, $room);
$response = $this->actingAs($sender)->postJson(route('gift.gold'), [
'to_user' => $receiver->username,
'room_id' => $room->id,
@@ -805,6 +908,52 @@ class ChatControllerTest extends TestCase
));
}
/**
* 测试目标用户不在当前房间时不能赠送金币。
*/
public function test_gift_gold_requires_target_online_in_same_room(): void
{
$room = Room::create(['room_name' => 'ggbd']);
$sender = User::factory()->create(['jjb' => 500]);
$receiver = User::factory()->create(['jjb' => 100]);
$this->joinRoom($sender, $room);
$response = $this->actingAs($sender)->postJson(route('gift.gold'), [
'to_user' => $receiver->username,
'room_id' => $room->id,
'amount' => 88,
]);
$response->assertStatus(403);
$this->assertSame(500, $sender->fresh()->jjb);
$this->assertSame(100, $receiver->fresh()->jjb);
}
/**
* 测试目标用户不在当前房间时不能送花。
*/
public function test_send_flower_requires_target_online_in_same_room(): void
{
$room = Room::create(['room_name' => 'flwbd']);
$sender = User::factory()->create(['jjb' => 500]);
$receiver = User::factory()->create(['meili' => 0]);
$gift = Gift::query()->where('is_active', true)->firstOrFail();
$this->joinRoom($sender, $room);
$response = $this->actingAs($sender)->postJson(route('gift.flower'), [
'to_user' => $receiver->username,
'room_id' => $room->id,
'gift_id' => $gift->id,
'count' => 1,
]);
$response->assertStatus(403);
$this->assertSame(500, $sender->fresh()->jjb);
$this->assertSame(0, $receiver->fresh()->meili);
}
/**
* 测试会员用户首次进房时会把专属欢迎主题写入历史消息。
*/
@@ -872,6 +1021,8 @@ class ChatControllerTest extends TestCase
]);
$room = Room::create(['room_name' => 'test_ann', 'room_owner' => 'someone']);
$this->joinRoom($user, $room);
$response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [
'announcement' => 'This is a new test announcement',
]);
@@ -892,6 +1043,8 @@ class ChatControllerTest extends TestCase
]);
$room = Room::create(['room_name' => 'test_ann2', 'room_owner' => 'other']);
$this->joinRoom($user, $room);
$response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [
'announcement' => 'This is a new test announcement',
]);
@@ -969,4 +1122,15 @@ class ChatControllerTest extends TestCase
return $user->fresh();
}
/**
* 将测试用户写入指定房间在线列表,模拟已经进入聊天室的状态。
*/
private function joinRoom(User $user, Room $room): void
{
Redis::hset("room:{$room->id}:users", $user->username, json_encode([
'user_id' => $user->id,
'username' => $user->username,
], JSON_UNESCAPED_UNICODE));
}
}