'testroom']); $user = User::factory()->create(); $response = $this->actingAs($user)->get(route('chat.room', $room->id)); $response->assertStatus(200); $response->assertViewIs('chat.frame'); // Assert user was added to room in redis $this->assertEquals(1, Redis::hexists("room:{$room->id}:users", $user->username)); } /** * 测试主干默认聊天室页面不会渲染虚拟形象挂载点和配置。 */ public function test_room_view_does_not_render_avatar_widget_or_config_by_default(): void { $room = Room::create(['room_name' => 'avguard']); $user = User::factory()->create(); $response = $this->actingAs($user)->get(route('chat.room', $room->id)); $response->assertOk(); $response->assertDontSee('chat-avatar-widget'); $response->assertDontSee('chatAvatarWidget'); } /** * 测试聊天室输入区会渲染系统播报屏蔽按钮与对应选项。 */ public function test_room_view_renders_block_system_sender_controls(): void { $room = Room::create(['room_name' => 'blockmenu']); $user = User::factory()->create(); $response = $this->actingAs($user)->get(route('chat.room', $room->id)); $response->assertOk(); $response->assertSee('🔕 屏蔽', false); $response->assertSee('🔇 禁音', false); $response->assertSee('钓鱼播报'); $response->assertSee('星海小博士'); $response->assertSee('百家乐'); $response->assertSee('跑马'); $response->assertSee('神秘箱子'); $response->assertSee('chat_blocked_system_senders'); $response->assertSee('toggleBlockedSystemSender'); } /** * 测试用户可以发送普通文本消息。 */ public function test_can_send_message() { $room = Room::create(['room_name' => 'test_send']); $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' => '#000000', 'action' => 'say', ]); $response->assertStatus(200); $response->assertJson(['status' => 'success']); // 查看 Redis 里的消息记录 $messages = Redis::lrange("room:{$room->id}:messages", 0, -1); $this->assertNotEmpty($messages); $found = false; foreach ($messages as $msgJson) { $msg = json_decode($msgJson, true); if ($msg['from_user'] === $user->username && $msg['content'] === '测试消息') { $found = true; break; } } $this->assertTrue($found, 'Message not found in Redis'); } /** * 测试文本内容为字符串 0 时仍可正常发送。 */ public function test_can_send_zero_message_content(): void { $room = Room::create(['room_name' => 'send0']); $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' => '0', 'is_secret' => false, 'font_color' => '#000000', 'action' => '', ]); $response->assertOk(); $response->assertJson(['status' => 'success']); $messages = Redis::lrange("room:{$room->id}:messages", 0, -1); $found = false; foreach ($messages as $msgJson) { $msg = json_decode($msgJson, true); if ($msg['from_user'] === $user->username && $msg['content'] === '0') { $found = true; break; } } $this->assertTrue($found, 'Zero message not found in Redis'); } /** * 测试用户可以发送带缩略图的图片消息。 */ public function test_can_send_image_message(): void { Storage::fake('public'); $room = Room::create(['room_name' => 'imgsend']); $user = User::factory()->create(); $this->actingAs($user)->get(route('chat.room', $room->id)); $response = $this->actingAs($user)->post(route('chat.send', $room->id), [ 'to_user' => '大家', 'content' => '图片说明', 'font_color' => '#000000', 'action' => '', 'image' => UploadedFile::fake()->image('chat-picture.png', 1280, 960), ], [ 'Accept' => 'application/json', ]); $response->assertOk(); $response->assertJson(['status' => 'success']); $messages = Redis::lrange("room:{$room->id}:messages", 0, -1); $payload = collect($messages) ->map(fn (string $item) => json_decode($item, true)) ->first(fn (array $item) => ($item['from_user'] ?? null) === $user->username && ($item['message_type'] ?? null) === 'image'); $this->assertNotNull($payload); $this->assertSame('image', $payload['message_type'] ?? null); $this->assertNotEmpty($payload['image_path'] ?? null); $this->assertNotEmpty($payload['image_thumb_path'] ?? null); $this->assertNotEmpty($payload['image_url'] ?? null); $this->assertNotEmpty($payload['image_thumb_url'] ?? null); Storage::disk('public')->assertExists($payload['image_path']); Storage::disk('public')->assertExists($payload['image_thumb_path']); } /** * 测试聊天室发送接口在 419 场景下会返回稳定的 JSON 提示。 */ public function test_chat_send_http_419_exception_renders_json_response(): void { $request = Request::create('/room/1/send', 'POST', server: [ 'HTTP_ACCEPT' => 'application/json', ]); $response = $this->app->make(\Illuminate\Contracts\Debug\ExceptionHandler::class) ->render($request, new HttpException(419, 'Page Expired')); \Illuminate\Testing\TestResponse::fromBaseResponse($response)->assertStatus(419)->assertJson([ 'status' => 'error', 'message' => '页面已过期,请刷新后重试。', ]); } /** * 测试超过保留期的聊天图片会被命令清理并改成过期占位消息。 */ public function test_purge_command_cleans_expired_chat_images(): void { Storage::fake('public'); Storage::disk('public')->put('chat-images/2026-04-08/sample_original.png', 'original'); Storage::disk('public')->put('chat-images/2026-04-08/sample_thumb.png', 'thumb'); $message = \App\Models\Message::create([ 'room_id' => 1, 'from_user' => 'tester', 'to_user' => '大家', 'content' => '历史图片', 'is_secret' => false, 'font_color' => '#000000', 'action' => '', 'message_type' => 'image', 'image_path' => 'chat-images/2026-04-08/sample_original.png', 'image_thumb_path' => 'chat-images/2026-04-08/sample_thumb.png', 'image_original_name' => 'sample.png', 'sent_at' => now()->subDays(4), ]); $this->artisan('messages:purge', [ '--days' => 30, '--image-days' => 3, ])->assertExitCode(0); $message->refresh(); $this->assertSame('expired_image', $message->message_type); $this->assertNull($message->image_path); $this->assertNull($message->image_thumb_path); $this->assertNull($message->image_original_name); $this->assertStringContainsString('图片已过期', $message->content); Storage::disk('public')->assertMissing('chat-images/2026-04-08/sample_original.png'); Storage::disk('public')->assertMissing('chat-images/2026-04-08/sample_thumb.png'); } /** * 测试心跳接口可以正常返回成功响应。 */ public function test_can_trigger_heartbeat() { $room = Room::create(['room_name' => 'test_hb']); $user = User::factory()->create(['exp_num' => 0]); $response = $this->actingAs($user)->postJson(route('chat.heartbeat', $room->id)); $response->assertStatus(200); $response->assertJsonFragment(['status' => 'success']); $user->refresh(); $this->assertGreaterThanOrEqual(0, $user->exp_num); // Might be 1 depending on sysparam } /** * 测试显式退房会清理 Redis 在线状态。 */ public function test_can_leave_room() { $room = Room::create(['room_name' => 'test_leave']); $user = User::factory()->create(); // 进房 $this->actingAs($user)->get(route('chat.room', $room->id)); $this->assertEquals(1, Redis::hexists("room:{$room->id}:users", $user->username)); // 显式退房 $response = $this->actingAs($user)->postJson(route('chat.leave', $room->id).'?explicit=1'); $response->assertStatus(200); // 缓存中被移除 $this->assertEquals(0, Redis::hexists("room:{$room->id}:users", $user->username)); } /** * 测试签名退房链接同样可以正常清理在线状态。 */ public function test_can_leave_room_through_signed_expired_route(): void { $room = Room::create(['room_name' => 'leave2']); $user = User::factory()->create(); $this->actingAs($user)->get(route('chat.room', $room->id)); $this->assertEquals(1, Redis::hexists("room:{$room->id}:users", $user->username)); $url = URL::temporarySignedRoute('chat.leave.expired', now()->addMinutes(5), [ 'id' => $room->id, 'user' => $user->id, ]); $response = $this->getJson($url); $response->assertStatus(200); $this->assertEquals(0, Redis::hexists("room:{$room->id}:users", $user->username)); } /** * 测试双击名片赠金币会写入私聊消息,且只有赠送双方能看到历史记录。 */ public function test_gift_gold_creates_secret_message_visible_only_to_participants(): void { $room = Room::create(['room_name' => 'gift-gold']); $sender = User::factory()->create(['jjb' => 500]); $receiver = User::factory()->create(['jjb' => 100]); $outsider = User::factory()->create(); $response = $this->actingAs($sender)->postJson(route('gift.gold'), [ 'to_user' => $receiver->username, 'room_id' => $room->id, 'amount' => 88, ]); $response->assertOk(); $response->assertJsonFragment([ 'status' => 'success', 'message' => "赠送成功!已向 {$receiver->username} 赠送 88 金币。", ]); // 余额应同步完成转移,确保消息不是“通知成功但金额未变”。 $this->assertSame(412, $sender->fresh()->jjb); $this->assertSame(188, $receiver->fresh()->jjb); $messages = Redis::lrange("room:{$room->id}:messages", 0, -1); $giftMessage = collect($messages) ->map(fn (string $item) => json_decode($item, true)) ->first(fn (array $item) => ($item['from_user'] ?? null) === $sender->username && ($item['to_user'] ?? null) === $receiver->username && ($item['content'] ?? null) === '赠送了你 88 金币!💝'); $this->assertNotNull($giftMessage); $this->assertTrue((bool) ($giftMessage['is_secret'] ?? false)); $this->assertSame('💰 赠金币到账', $giftMessage['toast_notification']['title'] ?? null); $this->assertSame('💰', $giftMessage['toast_notification']['icon'] ?? null); $this->assertSame('#f59e0b', $giftMessage['toast_notification']['color'] ?? null); // 赠送方查看房间历史时,应保留这条私聊通知。 $senderHistory = $this->actingAs($sender) ->get(route('chat.room', $room->id)) ->viewData('historyMessages'); $this->assertTrue(collect($senderHistory)->contains( fn (array $item) => ($item['from_user'] ?? null) === $sender->username && ($item['to_user'] ?? null) === $receiver->username && ($item['content'] ?? null) === '赠送了你 88 金币!💝' )); // 接收方查看房间历史时,也应看到这条私聊通知。 $receiverHistory = $this->actingAs($receiver) ->get(route('chat.room', $room->id)) ->viewData('historyMessages'); $this->assertTrue(collect($receiverHistory)->contains( fn (array $item) => ($item['from_user'] ?? null) === $sender->username && ($item['to_user'] ?? null) === $receiver->username && ($item['content'] ?? null) === '赠送了你 88 金币!💝' )); // 旁观者不应在历史消息里看到别人的赠金币私聊。 $outsiderHistory = $this->actingAs($outsider) ->get(route('chat.room', $room->id)) ->viewData('historyMessages'); $this->assertFalse(collect($outsiderHistory)->contains( fn (array $item) => ($item['from_user'] ?? null) === $sender->username && ($item['to_user'] ?? null) === $receiver->username && ($item['content'] ?? null) === '赠送了你 88 金币!💝' )); } /** * 测试会员用户首次进房时会把专属欢迎主题写入历史消息。 */ public function test_vip_user_join_message_uses_presence_theme_payload(): void { $room = Room::create(['room_name' => 'viproom']); $vipLevel = \App\Models\VipLevel::factory()->create([ 'join_effect' => 'lightning', 'join_banner_style' => 'storm', 'allow_custom_messages' => true, ]); $user = User::factory()->create([ 'vip_level_id' => $vipLevel->id, 'hy_time' => now()->addDays(30), 'custom_join_message' => '{username} 带着风暴王座闪耀降临', 'has_received_new_gift' => true, ]); $response = $this->actingAs($user)->get(route('chat.room', $room->id)); $response->assertStatus(200); $history = $response->viewData('historyMessages'); $presenceMessage = collect($history)->first(fn (array $message) => ($message['action'] ?? '') === 'vip_presence'); $this->assertNotNull($presenceMessage); $this->assertSame('join', $presenceMessage['presence_type']); $this->assertSame('lightning', $presenceMessage['presence_effect']); $this->assertSame('storm', $presenceMessage['presence_banner_style']); $this->assertStringContainsString($user->username, $presenceMessage['presence_text']); } /** * 测试可以获取所有房间的在线人数状态。 */ public function test_can_get_rooms_online_status() { $user = User::factory()->create(); $room1 = Room::create(['room_name' => 'room1']); $room2 = Room::create(['room_name' => 'room2']); $this->actingAs($user)->get(route('chat.room', $room1->id)); $response = $this->actingAs($user)->getJson(route('chat.rooms-online-status')); $response->assertStatus(200); // Assert room1 has 1 online, room2 has 0 $response->assertJsonFragment([ 'id' => $room1->id, 'online' => 1, ]); $response->assertJsonFragment([ 'id' => $room2->id, 'online' => 0, ]); } /** * 测试管理员可以设置房间公告。 */ public function test_can_set_announcement() { $user = User::factory()->create(['user_level' => 100]); // superadmin $room = Room::create(['room_name' => 'test_ann', 'room_owner' => 'someone']); $response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [ 'announcement' => 'This is a new test announcement', ]); $response->assertStatus(200); $room->refresh(); $this->assertStringContainsString('This is a new test announcement', $room->announcement); } /** * 测试无权限用户不能设置房间公告。 */ public function test_cannot_set_announcement_without_permission() { $user = User::factory()->create(['user_level' => 0]); $room = Room::create(['room_name' => 'test_ann2', 'room_owner' => 'someone']); $response = $this->actingAs($user)->postJson(route('chat.announcement', $room->id), [ 'announcement' => 'This is a new test announcement', ]); $response->assertStatus(403); } }