Files
chatroom/tests/Feature/ChatControllerTest.php

352 lines
12 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Tests\Feature;
use App\Models\Room;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Facades\URL;
use Tests\TestCase;
/**
* 聊天室控制器功能测试
* 覆盖进房、发言、图片消息与退房等关键聊天流程。
*/
class ChatControllerTest extends TestCase
{
use RefreshDatabase;
/**
* 每个测试前清空 Redis避免跨用例污染在线状态与消息缓存。
*/
protected function setUp(): void
{
parent::setUp();
Redis::flushall();
}
/**
* 测试用户可以正常进入聊天室页面。
*/
public function test_can_view_room()
{
$room = Room::create(['room_name' => '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_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']);
}
/**
* 测试超过保留期的聊天图片会被命令清理并改成过期占位消息。
*/
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_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);
}
}